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.
- package/CHANGELOG.md +11 -185
- package/LIESMICH.txt +51 -13
- package/README.md +44 -47
- package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
- package/agent-spine-plugin/CHANGELOG.md +37 -5
- package/agent-spine-plugin/README.md +3 -3
- package/agent-spine-plugin/blun.plugin.json +45 -10
- package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
- package/agent-spine-plugin/docs/host-integration.md +42 -27
- package/agent-spine-plugin/docs/preflight-recall.md +4 -2
- package/agent-spine-plugin/docs/session-timeline.md +97 -236
- package/agent-spine-plugin/docs/world-model.md +25 -0
- package/agent-spine-plugin/hooks/codex.json +1 -1
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/package.json +1 -3
- package/agent-spine-plugin/scripts/check-hosts.js +3 -3
- package/agent-spine-plugin/scripts/release-check.js +10 -5
- package/agent-spine-plugin/scripts/run-checks.js +4 -1
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
- package/agent-spine-plugin/src/cli-learning.js +15 -0
- package/agent-spine-plugin/src/cli.js +2 -0
- package/agent-spine-plugin/src/hook.js +32 -32
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
- package/agent-spine-plugin/src/lib/briefing.js +146 -36
- package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
- package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
- package/agent-spine-plugin/src/lib/hook-context.js +16 -3
- package/agent-spine-plugin/src/lib/hook-output.js +129 -5
- package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
- package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
- package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
- package/agent-spine-plugin/src/lib/learning-context.js +11 -4
- package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
- package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
- package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
- package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
- package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
- package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
- package/agent-spine-plugin/src/lib/preflight.js +4 -4
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
- package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
- package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
- package/agent-spine-plugin/src/lib/source-roots.js +3 -2
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
- package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
- package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
- package/agent-spine-plugin/src/lib/world-model.js +64 -9
- package/agent-spine-plugin/src/worker.js +13 -1
- package/bin/blun.js +43 -28
- package/bin/core-bootstrap.js +5 -4
- package/bin/king.js +43 -28
- package/bin/launcher-mode.js +1 -10
- package/bin/launcher-runtime.js +128 -295
- package/bin/managed-node.js +0 -0
- package/bin/managed-plugin-selection.cjs +0 -1
- package/bin/native-module-repair.js +0 -0
- package/bin/node-runtime.js +0 -0
- package/bin/node-version.js +0 -0
- package/bin/plugin-bootstrap.js +56 -120
- package/bin/private-paths.js +11 -34
- package/bin/standard-tools-bootstrap.js +34 -114
- package/bin/turn-thinking-policy.cjs +3 -11
- package/bin/update-copy.js +200 -0
- package/bin/update-lease.js +0 -0
- package/bin/update-notice.js +136 -289
- package/bin/verify-agent-behavior.cjs +122 -0
- package/bin/verify-agent-components.cjs +104 -0
- package/bin/verify-bundled-agent-sources.cjs +57 -0
- package/blun.mjs +143076 -135288
- package/bundled-agent-sources.json +701 -0
- package/package.json +12 -15
- package/standard-skills/translate-native/README.md +1293 -0
- package/standard-skills/translate-native/SKILL.md +172 -22
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/agents/openai.yaml +18 -0
- package/standard-skills/translate-native/assets/icon.svg +8 -0
- package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
- package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
- package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
- package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
- package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
- package/standard-skills/translate-native/integrations/audit_log.py +327 -0
- package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
- package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
- package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
- package/standard-skills/translate-native/integrations/guard_service.py +435 -0
- package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
- package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
- package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
- package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
- package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
- package/standard-skills/translate-native/integrations/task_router.py +101 -0
- package/standard-skills/translate-native/integrations/website_localization.py +401 -0
- package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
- package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
- package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
- package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
- package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
- package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
- package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
- package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
- package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
- package/standard-skills/translate-native/provenance.json +3 -4
- package/standard-skills/translate-native/references/commercial-localization.md +177 -0
- package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/LICENSE +21 -0
- package/standard-tools/language-guard/VERSION +1 -0
- package/standard-tools/language-guard/blun_language_guard.py +7 -1
- package/standard-tools/language-guard/check_commercial_review.py +80 -0
- package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/language_gateway.py +62 -0
- package/standard-tools/language-guard/pre_output_guard.py +64 -0
- package/standard-tools/language-guard/provenance.json +4 -11
- package/standard-tools/manifest.json +34 -11
- package/telegram-plugin/commands/access.md +2 -10
- package/telegram-plugin/dist/bridge.mjs +64041 -687
- package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
- package/telegram-plugin/dist/noise.mjs +28 -63511
- package/agent-spine-plugin/CONTRIBUTING.md +0 -52
- package/agent-spine-plugin/SECURITY.md +0 -47
- package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
- package/agent-spine-plugin/docs/releasing.md +0 -85
- package/agent-spine-plugin/docs/structured-completion.md +0 -67
- package/bin/abort-listener-policy.cjs +0 -43
- package/bin/active-steer-priority-policy.cjs +0 -24
- package/bin/agent-api-http-adapter.mjs +0 -446
- package/bin/agent-api-private-http-server.mjs +0 -288
- package/bin/agent-api-runtime.mjs +0 -252
- package/bin/agent-api-service-environment.mjs +0 -236
- package/bin/agent-api-service-host.mjs +0 -209
- package/bin/agent-api-service-process.mjs +0 -171
- package/bin/agent-api-session-registry.mjs +0 -428
- package/bin/agent-api-tool-broker.cjs +0 -248
- package/bin/agent-api-turn-controller.mjs +0 -461
- package/bin/agent-api-usage-journal.cjs +0 -259
- package/bin/agent-resume-snapshot.cjs +0 -241
- package/bin/agentspine-king-goal-inbox.mjs +0 -111
- package/bin/agentspine-king-goal-intake.mjs +0 -106
- package/bin/approval-rejection-stop.cjs +0 -15
- package/bin/assistant-message-offload-policy.cjs +0 -284
- package/bin/baseline-skill-performance-policy.cjs +0 -39
- package/bin/bash-search-scope-policy.cjs +0 -49
- package/bin/codebase-search-runtime.cjs +0 -23
- package/bin/cognitive-action-checkpoint.cjs +0 -1104
- package/bin/cognitive-attention-delivery.cjs +0 -76
- package/bin/cognitive-attention-policy.cjs +0 -143
- package/bin/cognitive-attention-runtime.cjs +0 -91
- package/bin/cognitive-context-projection.cjs +0 -73
- package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
- package/bin/cognitive-effective-view.cjs +0 -77
- package/bin/cognitive-focus-projection.cjs +0 -206
- package/bin/cognitive-focus-scope.cjs +0 -37
- package/bin/cognitive-goal-autostart-policy.cjs +0 -72
- package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
- package/bin/cognitive-memory-adapter.cjs +0 -282
- package/bin/cognitive-memory-command.cjs +0 -293
- package/bin/cognitive-memory-provider.cjs +0 -92
- package/bin/cognitive-salience-policy.cjs +0 -159
- package/bin/cognitive-state-store.cjs +0 -508
- package/bin/cognitive-turn-lifecycle.cjs +0 -624
- package/bin/cognitive-work-focus.cjs +0 -180
- package/bin/compaction-history-archive.cjs +0 -166
- package/bin/compaction-history-startup.cjs +0 -50
- package/bin/compaction-model-policy.cjs +0 -31
- package/bin/compaction-stage-policy.cjs +0 -21
- package/bin/compaction-transaction-policy.cjs +0 -122
- package/bin/config-write-dedup-policy.cjs +0 -27
- package/bin/context-budget-ledger.cjs +0 -31
- package/bin/context-doctor-policy.cjs +0 -70
- package/bin/context-insight-policy.cjs +0 -36
- package/bin/context-performance-policy.cjs +0 -19
- package/bin/context-pressure-policy.cjs +0 -20
- package/bin/cron-run-output.cjs +0 -45
- package/bin/cron-run-store.cjs +0 -145
- package/bin/curiosity-scout-policy.cjs +0 -49
- package/bin/default-model-output-budget-policy.cjs +0 -28
- package/bin/durable-task-resume-policy.cjs +0 -130
- package/bin/durable-task-resume-runtime.cjs +0 -117
- package/bin/durable-task-resume-store.cjs +0 -88
- package/bin/editable-tool-approval-policy.cjs +0 -540
- package/bin/editable-tool-approval-runtime.cjs +0 -99
- package/bin/effective-system-prompt-cache-policy.cjs +0 -33
- package/bin/error-memory-performance-policy.cjs +0 -113
- package/bin/file-observation-policy.cjs +0 -133
- package/bin/foreground-output-capture-policy.cjs +0 -41
- package/bin/generated-source-health.cjs +0 -142
- package/bin/glob-pattern-policy.cjs +0 -13
- package/bin/goal-completion-evidence-policy.cjs +0 -120
- package/bin/grep-output-limit-policy.cjs +0 -39
- package/bin/historical-media-projection-policy.cjs +0 -48
- package/bin/history-offload-pressure-policy.cjs +0 -33
- package/bin/html-to-research-markdown.cjs +0 -147
- package/bin/identity-context-policy.cjs +0 -764
- package/bin/identity-journal-policy.cjs +0 -107
- package/bin/input-draft-persistence.cjs +0 -77
- package/bin/king-tui-function-contract.json +0 -33
- package/bin/launcher-restart-policy.cjs +0 -150
- package/bin/live-response-repetition-guard.cjs +0 -196
- package/bin/llm-config-log-dedup-policy.cjs +0 -76
- package/bin/loop-event-record-policy.cjs +0 -174
- package/bin/managed-context-startup-policy.cjs +0 -27
- package/bin/media-activity-layout-policy.cjs +0 -34
- package/bin/media-auto-retrieval-policy.cjs +0 -90
- package/bin/media-result-policy.cjs +0 -59
- package/bin/micro-compaction-policy.cjs +0 -145
- package/bin/mistake-relevance-policy.cjs +0 -319
- package/bin/model-retry-progress-policy.cjs +0 -46
- package/bin/native-large-file-io.cjs +0 -42
- package/bin/native-runtime-cache.cjs +0 -76
- package/bin/natural-presence-policy.cjs +0 -28
- package/bin/noninteractive-shell-env-policy.cjs +0 -19
- package/bin/observer-hooks.cjs +0 -14
- package/bin/outbound-claim-provenance.cjs +0 -150
- package/bin/oversized-context-offload-policy.cjs +0 -86
- package/bin/pending-media-policy.cjs +0 -182
- package/bin/pending-token-estimate-policy.cjs +0 -41
- package/bin/personal-memory-consent-policy.cjs +0 -72
- package/bin/personal-memory-performance-policy.cjs +0 -12
- package/bin/personality-choice-policy.cjs +0 -101
- package/bin/personality-memory-adapter.cjs +0 -379
- package/bin/personality-mode.cjs +0 -46
- package/bin/personality-setup-policy.cjs +0 -197
- package/bin/proactive-compaction-policy.cjs +0 -25
- package/bin/profile-identity-resolution.cjs +0 -136
- package/bin/profile-runtime.cjs +0 -318
- package/bin/profile-tool-exclusion-policy.cjs +0 -37
- package/bin/programmatic-context-isolation.cjs +0 -25
- package/bin/programmatic-tool-runtime.mjs +0 -627
- package/bin/provider-idle-timeout-policy.cjs +0 -14
- package/bin/provider-model-refresh-deadline.cjs +0 -53
- package/bin/provider-model-refresh-policy.cjs +0 -107
- package/bin/rate-limit-recovery-policy.cjs +0 -47
- package/bin/read-batch-policy.cjs +0 -32
- package/bin/read-continuation-policy.cjs +0 -59
- package/bin/recurring-cron-history-policy.cjs +0 -124
- package/bin/relationship-continuity-policy.cjs +0 -143
- package/bin/relationship-curiosity-policy.cjs +0 -107
- package/bin/relationship-learning-policy.cjs +0 -168
- package/bin/release-artifact-freeze-policy.cjs +0 -30
- package/bin/reload-plugin-bootstrap.cjs +0 -18
- package/bin/reload-queue-policy.cjs +0 -38
- package/bin/repeated-assistant-response-policy.cjs +0 -232
- package/bin/repeated-injection-projection.cjs +0 -107
- package/bin/repeated-user-message-projection.cjs +0 -8
- package/bin/research-page-result.cjs +0 -74
- package/bin/retry-checkpoint-policy.cjs +0 -13
- package/bin/runtime-exit-ledger.cjs +0 -144
- package/bin/scoped-cron-run-policy.cjs +0 -358
- package/bin/session-checkpoint-policy.cjs +0 -25
- package/bin/session-compaction-policy.cjs +0 -84
- package/bin/session-replay-policy.cjs +0 -20
- package/bin/session-replay-window-policy.cjs +0 -40
- package/bin/session-resume-checkpoint.cjs +0 -254
- package/bin/session-scrollback-archive.cjs +0 -229
- package/bin/skill-activation-performance-policy.cjs +0 -69
- package/bin/skill-listing-performance-policy.cjs +0 -92
- package/bin/soul-organization-policy.cjs +0 -78
- package/bin/soul-preservation-policy.cjs +0 -20
- package/bin/startup-preferences.cjs +0 -131
- package/bin/streaming-flush-performance-policy.cjs +0 -28
- package/bin/structured-agent-swarm-output.cjs +0 -325
- package/bin/structured-subagent-output.cjs +0 -252
- package/bin/subagent-context-fork-policy.cjs +0 -155
- package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
- package/bin/subagent-parent-responsiveness.cjs +0 -19
- package/bin/subagent-skill-policy.cjs +0 -206
- package/bin/subagent-timeout-policy.cjs +0 -182
- package/bin/subagent-tool-policy.cjs +0 -60
- package/bin/subagent-usage-rollup-policy.cjs +0 -29
- package/bin/system-prompt-context-policy.cjs +0 -124
- package/bin/system-prompt-token-cache-policy.cjs +0 -60
- package/bin/telegram-addressed-focus.cjs +0 -55
- package/bin/telegram-addressed-priority.cjs +0 -12
- package/bin/telegram-approval-relay.cjs +0 -290
- package/bin/telegram-bot-priority.cjs +0 -17
- package/bin/telegram-console-status-policy.cjs +0 -174
- package/bin/telegram-context-projection-policy.cjs +0 -141
- package/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/bin/telegram-direct-focus-policy.cjs +0 -273
- package/bin/telegram-mcp-compatibility.cjs +0 -49
- package/bin/telegram-media-delivery-policy.cjs +0 -42
- package/bin/telegram-private-conversation-policy.cjs +0 -185
- package/bin/telegram-queue-handoff-policy.cjs +0 -73
- package/bin/telegram-remote-status-policy.cjs +0 -120
- package/bin/telegram-session-queue-runtime.mjs +0 -306
- package/bin/telegram-text-chunk-policy.cjs +0 -63
- package/bin/telegram-truncated-reply-policy.cjs +0 -37
- package/bin/telegram-urgent-policy.cjs +0 -45
- package/bin/telemetry-spool-policy.cjs +0 -57
- package/bin/thinking-activity-status-policy.cjs +0 -132
- package/bin/thinking-only-guard.cjs +0 -80
- package/bin/todo-list-turn-policy.cjs +0 -131
- package/bin/tool-call-loop-policy.cjs +0 -51
- package/bin/tool-file-persistence.cjs +0 -141
- package/bin/tool-result-offload-policy.cjs +0 -359
- package/bin/tool-result-offload-telemetry.cjs +0 -12
- package/bin/tool-schema-token-cache-policy.cjs +0 -41
- package/bin/tool-stream-preview-policy.cjs +0 -9
- package/bin/tui-functional-contract.cjs +0 -55
- package/bin/turn-tool-performance-policy.cjs +0 -486
- package/bin/usage-cache-efficiency-policy.cjs +0 -26
- package/bin/user-home-path-policy.cjs +0 -13
- package/bin/user-message-offload-policy.cjs +0 -103
- package/bin/user-prompt-hook-origin-policy.cjs +0 -34
- package/bin/user-tool-record-policy.cjs +0 -7
- package/bin/validated-learning-insight-policy.cjs +0 -58
- package/bin/validated-learning-outcome-trace.cjs +0 -107
- package/bin/validated-learning-performance-policy.cjs +0 -53
- package/bin/validated-learning-signal.cjs +0 -463
- package/bin/windows-bash-dialect-policy.cjs +0 -25
- package/bin/windows-node-crash-dump.cjs +0 -110
- package/bin/write-continuation-policy.cjs +0 -69
- package/codebase-index/README.md +0 -82
- package/codebase-index/codebase_index.py +0 -470
- package/standard-skills/agent-browser/SKILL.md +0 -19
- package/standard-skills/agent-browser/references/runtime.md +0 -8
- package/standard-skills/blun-session-inspector/SKILL.md +0 -41
- package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
- package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
- package/standard-skills/full-output-enforcement/SKILL.md +0 -49
- package/standard-skills/high-end-visual-design/SKILL.md +0 -98
- package/standard-skills/image-to-code/SKILL.md +0 -1228
- package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
- package/standard-skills/minimalist-ui/SKILL.md +0 -85
- package/standard-skills/motion-design-taste/SKILL.md +0 -74
- package/standard-skills/playwright-testing/SKILL.md +0 -19
- package/standard-skills/playwright-testing/references/runtime.md +0 -7
- package/standard-skills/premortem/SKILL.md +0 -148
- package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
- package/standard-skills/research-evidence/SKILL.md +0 -39
- package/standard-skills/research-evidence/references/evidence-format.md +0 -104
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
- package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
- package/standard-skills/screenshot-lesen/SKILL.md +0 -52
- package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
- package/standard-skills/stitch-design-taste/SKILL.md +0 -184
- package/standard-skills/telegram-channel/SKILL.md +0 -18
- package/standard-skills/telegram-channel/references/runtime.md +0 -7
- package/standard-skills/venture-flywheel/SKILL.md +0 -32
- package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
- package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
- package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
- package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
- package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
- package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
- package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
- package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
- package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
- package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
- package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
- package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
- package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
- package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
- package/standard-skills/web-lesen/SKILL.md +0 -73
- package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
- package/standard-skills/windows-mcp/SKILL.md +0 -19
- package/standard-skills/windows-mcp/references/runtime.md +0 -9
- package/telegram-plugin/DELIVERY.md +0 -36
- package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
- package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
- package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
- package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
- package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
- package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
- package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
- /package/{bin → scripts}/fix-node-pty-perms.js +0 -0
|
@@ -0,0 +1,1541 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only, content-free health and readiness monitor for localization.
|
|
3
|
+
|
|
4
|
+
The monitor validates durable queue and quality-evidence state, authenticated
|
|
5
|
+
CMS metadata, signed approvals, publication outbox entries, and provider
|
|
6
|
+
availability without ever returning source text, target text, reviewer prose,
|
|
7
|
+
or transport exceptions.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import sqlite3
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import asdict, dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Mapping, Protocol
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SCHEMA = "blun.website-localization-health.v1"
|
|
24
|
+
PROVIDER_HEALTH_SCHEMA = "blun.localization-provider-health.v1"
|
|
25
|
+
SUPERVISOR_SCHEMA = "blun.website-localization-supervisor.v1"
|
|
26
|
+
SUPERVISOR_PHASES = {
|
|
27
|
+
"tombstone", "delivery", "release", "translation", "idle", "supervisor",
|
|
28
|
+
}
|
|
29
|
+
SUPERVISOR_STATUSES = {
|
|
30
|
+
"idle", "succeeded", "retry_wait", "failed", "blocked", "approved",
|
|
31
|
+
"delivery_ready", "delivered",
|
|
32
|
+
}
|
|
33
|
+
QUEUE_STATUSES = ("pending", "leased", "retry_wait", "succeeded", "failed")
|
|
34
|
+
DELIVERY_STATUSES = ("pending", "leased", "retry_wait", "succeeded", "failed")
|
|
35
|
+
EVIDENCE_STATUSES = ("pending", "leased", "retry_wait", "succeeded", "failed")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _load_module(name: str, path: Path):
|
|
39
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
40
|
+
if spec is None or spec.loader is None:
|
|
41
|
+
raise RuntimeError(f"cannot load required health dependency: {path.name}")
|
|
42
|
+
module = importlib.util.module_from_spec(spec)
|
|
43
|
+
sys.modules[spec.name] = module
|
|
44
|
+
spec.loader.exec_module(module)
|
|
45
|
+
return module
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
49
|
+
_CMS = _load_module(
|
|
50
|
+
"blun_website_localization_health_cms",
|
|
51
|
+
_ROOT / "integrations" / "website_localization_cms.py",
|
|
52
|
+
)
|
|
53
|
+
_QUEUE = _CMS._QUEUE
|
|
54
|
+
_RELEASE = _CMS._RELEASE
|
|
55
|
+
_COORDINATOR = _load_module(
|
|
56
|
+
"blun_website_localization_health_coordinator",
|
|
57
|
+
_ROOT / "integrations" / "website_localization_release_coordinator.py",
|
|
58
|
+
)
|
|
59
|
+
_CAMPAIGN = _load_module(
|
|
60
|
+
"blun_website_localization_health_benchmark_campaign",
|
|
61
|
+
_ROOT / "integrations" / "website_localization_benchmark_campaign.py",
|
|
62
|
+
)
|
|
63
|
+
_BENCHMARK_REVIEW = _load_module(
|
|
64
|
+
"blun_website_localization_health_benchmark_review",
|
|
65
|
+
_ROOT / "integrations" / "website_localization_benchmark_review_store.py",
|
|
66
|
+
)
|
|
67
|
+
_REFERENCE_QUEUE = _load_module(
|
|
68
|
+
"blun_website_localization_health_native_reference_queue",
|
|
69
|
+
_ROOT / "integrations" / "website_localization_native_reference_queue.py",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LocalizationHealthBlocked(RuntimeError):
|
|
74
|
+
"""Raised only when the monitor itself receives an invalid dependency."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ProviderHealthProbe(Protocol):
|
|
78
|
+
def check(self, *, provider_id: str, model_id: str, model_version: str) -> Mapping[str, Any]: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PublisherHealthProbe(Protocol):
|
|
82
|
+
def check(self, *, contract_sha256: str) -> Mapping[str, Any]: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class ComponentHealth:
|
|
87
|
+
component: str
|
|
88
|
+
status: str
|
|
89
|
+
reasons: tuple[str, ...]
|
|
90
|
+
counts: tuple[tuple[str, int], ...]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class ProviderHealth:
|
|
95
|
+
provider_id: str
|
|
96
|
+
model_id: str
|
|
97
|
+
model_version: str
|
|
98
|
+
status: str
|
|
99
|
+
reason: str | None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class WebsiteVersionHealth:
|
|
104
|
+
event_id: str
|
|
105
|
+
site_id: str
|
|
106
|
+
website_version: str
|
|
107
|
+
plan_id: str
|
|
108
|
+
status: str
|
|
109
|
+
required_locales: int
|
|
110
|
+
approved_locales: int
|
|
111
|
+
queue_counts: tuple[tuple[str, int], ...]
|
|
112
|
+
blocked_locales: tuple[tuple[str, str], ...]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class LocalizationHealthReport:
|
|
117
|
+
checked_at: float
|
|
118
|
+
status: str
|
|
119
|
+
components: tuple[ComponentHealth, ...]
|
|
120
|
+
providers: tuple[ProviderHealth, ...]
|
|
121
|
+
website_versions: tuple[WebsiteVersionHealth, ...]
|
|
122
|
+
|
|
123
|
+
def as_payload(self) -> dict[str, Any]:
|
|
124
|
+
return {
|
|
125
|
+
"schema": SCHEMA,
|
|
126
|
+
"checked_at": self.checked_at,
|
|
127
|
+
"status": self.status,
|
|
128
|
+
"components": [
|
|
129
|
+
{
|
|
130
|
+
"component": item.component,
|
|
131
|
+
"status": item.status,
|
|
132
|
+
"reasons": list(item.reasons),
|
|
133
|
+
"counts": dict(item.counts),
|
|
134
|
+
}
|
|
135
|
+
for item in self.components
|
|
136
|
+
],
|
|
137
|
+
"providers": [asdict(item) for item in self.providers],
|
|
138
|
+
"website_versions": [
|
|
139
|
+
{
|
|
140
|
+
**asdict(item),
|
|
141
|
+
"queue_counts": dict(item.queue_counts),
|
|
142
|
+
"blocked_locales": [list(blocked) for blocked in item.blocked_locales],
|
|
143
|
+
}
|
|
144
|
+
for item in self.website_versions
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _timestamp(value: Any) -> float:
|
|
150
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
151
|
+
raise LocalizationHealthBlocked("checked_at must be a finite timestamp")
|
|
152
|
+
value = float(value)
|
|
153
|
+
if value < 0 or value != value or value in {float("inf"), float("-inf")}:
|
|
154
|
+
raise LocalizationHealthBlocked("checked_at must be a finite timestamp")
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _canonical_json(value: Any) -> str:
|
|
159
|
+
return json.dumps(
|
|
160
|
+
value,
|
|
161
|
+
ensure_ascii=False,
|
|
162
|
+
allow_nan=False,
|
|
163
|
+
sort_keys=True,
|
|
164
|
+
separators=(",", ":"),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _hash(value: str) -> str:
|
|
169
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _counts(connection: sqlite3.Connection, table: str, statuses: tuple[str, ...]) -> dict[str, int]:
|
|
173
|
+
result = {status: 0 for status in statuses}
|
|
174
|
+
rows = connection.execute(
|
|
175
|
+
f"SELECT status, COUNT(*) AS count FROM {table} GROUP BY status"
|
|
176
|
+
).fetchall()
|
|
177
|
+
for row in rows:
|
|
178
|
+
if row["status"] not in result:
|
|
179
|
+
raise ValueError("unknown status")
|
|
180
|
+
result[row["status"]] = int(row["count"])
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _component(name: str, status: str, reasons: set[str], counts: dict[str, int]) -> ComponentHealth:
|
|
185
|
+
return ComponentHealth(
|
|
186
|
+
name,
|
|
187
|
+
status,
|
|
188
|
+
tuple(sorted(reasons)),
|
|
189
|
+
tuple(sorted((key, int(value)) for key, value in counts.items())),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class LocalizationHealthMonitor:
|
|
194
|
+
"""Inspect a configured localization bridge without mutating its state."""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
bridge: Any,
|
|
199
|
+
evidence_state: Any | None = None,
|
|
200
|
+
supervisor: Any | None = None,
|
|
201
|
+
supervisor_stale_after_seconds: float | int = 30,
|
|
202
|
+
benchmark_store: Any | None = None,
|
|
203
|
+
benchmark_policy: Any | None = None,
|
|
204
|
+
benchmark_campaign_id: str | None = None,
|
|
205
|
+
benchmark_evidence_authority: Any | None = None,
|
|
206
|
+
benchmark_stale_after_seconds: float | int = 3600,
|
|
207
|
+
benchmark_review_store: Any | None = None,
|
|
208
|
+
benchmark_reviewer_route_id: str | None = None,
|
|
209
|
+
benchmark_reference_queue: Any | None = None,
|
|
210
|
+
):
|
|
211
|
+
if not self._supports_bridge(bridge):
|
|
212
|
+
raise LocalizationHealthBlocked("bridge must be WebsiteLocalizationCMSBridge")
|
|
213
|
+
if evidence_state is not None and not isinstance(
|
|
214
|
+
getattr(evidence_state, "connection", None), sqlite3.Connection,
|
|
215
|
+
):
|
|
216
|
+
raise LocalizationHealthBlocked("evidence state must use SQLite")
|
|
217
|
+
if supervisor is not None and not callable(getattr(supervisor, "status", None)):
|
|
218
|
+
raise LocalizationHealthBlocked("supervisor must expose read-only status")
|
|
219
|
+
benchmark_values = (
|
|
220
|
+
benchmark_store, benchmark_policy, benchmark_campaign_id,
|
|
221
|
+
benchmark_evidence_authority,
|
|
222
|
+
)
|
|
223
|
+
if any(value is not None for value in benchmark_values) and any(
|
|
224
|
+
value is None for value in benchmark_values
|
|
225
|
+
):
|
|
226
|
+
raise LocalizationHealthBlocked("benchmark configuration is incomplete")
|
|
227
|
+
if benchmark_store is not None and (
|
|
228
|
+
not isinstance(getattr(benchmark_store, "connection", None), sqlite3.Connection)
|
|
229
|
+
or not callable(getattr(benchmark_store, "health", None))
|
|
230
|
+
or not callable(getattr(benchmark_store, "_verify_schema", None))
|
|
231
|
+
):
|
|
232
|
+
raise LocalizationHealthBlocked("benchmark store is invalid")
|
|
233
|
+
if benchmark_store is not None and not callable(
|
|
234
|
+
getattr(benchmark_evidence_authority, "verify", None),
|
|
235
|
+
):
|
|
236
|
+
raise LocalizationHealthBlocked("benchmark authority is invalid")
|
|
237
|
+
review_values = (benchmark_review_store, benchmark_reviewer_route_id)
|
|
238
|
+
if any(value is not None for value in review_values) and any(
|
|
239
|
+
value is None for value in review_values
|
|
240
|
+
):
|
|
241
|
+
raise LocalizationHealthBlocked("benchmark review configuration is incomplete")
|
|
242
|
+
if benchmark_review_store is not None and benchmark_store is None:
|
|
243
|
+
raise LocalizationHealthBlocked("benchmark review requires campaign configuration")
|
|
244
|
+
if benchmark_reference_queue is not None and benchmark_store is None:
|
|
245
|
+
raise LocalizationHealthBlocked(
|
|
246
|
+
"native-reference queue requires campaign configuration",
|
|
247
|
+
)
|
|
248
|
+
if benchmark_review_store is not None and (
|
|
249
|
+
not isinstance(
|
|
250
|
+
getattr(benchmark_review_store, "connection", None),
|
|
251
|
+
sqlite3.Connection,
|
|
252
|
+
)
|
|
253
|
+
or not callable(getattr(benchmark_review_store, "health", None))
|
|
254
|
+
or not callable(getattr(benchmark_review_store, "_verify_schema", None))
|
|
255
|
+
or not isinstance(benchmark_reviewer_route_id, str)
|
|
256
|
+
or _CAMPAIGN._BENCHMARK.IDENTIFIER.fullmatch(
|
|
257
|
+
benchmark_reviewer_route_id,
|
|
258
|
+
) is None
|
|
259
|
+
):
|
|
260
|
+
raise LocalizationHealthBlocked("benchmark review store is invalid")
|
|
261
|
+
if benchmark_reference_queue is not None and (
|
|
262
|
+
getattr(
|
|
263
|
+
getattr(benchmark_reference_queue, "native_reference_queue", None),
|
|
264
|
+
"connection",
|
|
265
|
+
None,
|
|
266
|
+
) is not benchmark_store.connection
|
|
267
|
+
or not callable(getattr(
|
|
268
|
+
benchmark_reference_queue, "native_reference_queue_health", None,
|
|
269
|
+
))
|
|
270
|
+
):
|
|
271
|
+
raise LocalizationHealthBlocked("native-reference queue is invalid")
|
|
272
|
+
if (
|
|
273
|
+
isinstance(supervisor_stale_after_seconds, bool)
|
|
274
|
+
or not isinstance(supervisor_stale_after_seconds, (int, float))
|
|
275
|
+
or not math.isfinite(float(supervisor_stale_after_seconds))
|
|
276
|
+
or float(supervisor_stale_after_seconds) <= 0
|
|
277
|
+
):
|
|
278
|
+
raise LocalizationHealthBlocked("supervisor stale threshold is invalid")
|
|
279
|
+
if (
|
|
280
|
+
isinstance(benchmark_stale_after_seconds, bool)
|
|
281
|
+
or not isinstance(benchmark_stale_after_seconds, (int, float))
|
|
282
|
+
or not math.isfinite(float(benchmark_stale_after_seconds))
|
|
283
|
+
or not 0 < float(benchmark_stale_after_seconds) <= _CAMPAIGN.MAX_STALE_SECONDS
|
|
284
|
+
):
|
|
285
|
+
raise LocalizationHealthBlocked("benchmark stale threshold is invalid")
|
|
286
|
+
self.bridge = bridge
|
|
287
|
+
self.queue = bridge.queue
|
|
288
|
+
self.release_store = bridge.release_store
|
|
289
|
+
self.evidence_state = evidence_state
|
|
290
|
+
self.supervisor = supervisor
|
|
291
|
+
self.supervisor_stale_after_seconds = float(supervisor_stale_after_seconds)
|
|
292
|
+
self.benchmark_store = benchmark_store
|
|
293
|
+
self.benchmark_policy = benchmark_policy
|
|
294
|
+
self.benchmark_campaign_id = benchmark_campaign_id
|
|
295
|
+
self.benchmark_evidence_authority = benchmark_evidence_authority
|
|
296
|
+
self.benchmark_stale_after_seconds = float(benchmark_stale_after_seconds)
|
|
297
|
+
self.benchmark_review_store = benchmark_review_store
|
|
298
|
+
self.benchmark_reviewer_route_id = benchmark_reviewer_route_id
|
|
299
|
+
self.benchmark_reference_queue = benchmark_reference_queue
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def _supports_bridge(bridge: Any) -> bool:
|
|
303
|
+
queue = getattr(bridge, "queue", None)
|
|
304
|
+
release_store = getattr(bridge, "release_store", None)
|
|
305
|
+
return (
|
|
306
|
+
isinstance(getattr(bridge, "connection", None), sqlite3.Connection)
|
|
307
|
+
and isinstance(getattr(queue, "connection", None), sqlite3.Connection)
|
|
308
|
+
and isinstance(getattr(release_store, "connection", None), sqlite3.Connection)
|
|
309
|
+
and all(callable(getattr(bridge, name, None)) for name in (
|
|
310
|
+
"_verify_schema", "_load_event", "delivery_status",
|
|
311
|
+
))
|
|
312
|
+
and all(callable(getattr(queue, name, None)) for name in (
|
|
313
|
+
"_verify_schema", "plan_counts", "result", "status",
|
|
314
|
+
))
|
|
315
|
+
and all(callable(getattr(release_store, name, None)) for name in (
|
|
316
|
+
"lookup", "readiness", "validated_result",
|
|
317
|
+
))
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _quick_check(connection: sqlite3.Connection) -> bool:
|
|
322
|
+
row = connection.execute("PRAGMA quick_check(1)").fetchone()
|
|
323
|
+
return row is not None and row[0] == "ok"
|
|
324
|
+
|
|
325
|
+
def _check_schemas(self) -> set[str]:
|
|
326
|
+
reasons: set[str] = set()
|
|
327
|
+
try:
|
|
328
|
+
self.queue._verify_schema()
|
|
329
|
+
except Exception:
|
|
330
|
+
reasons.add("queue.schema_invalid")
|
|
331
|
+
try:
|
|
332
|
+
columns = tuple(
|
|
333
|
+
row["name"]
|
|
334
|
+
for row in self.release_store.connection.execute(
|
|
335
|
+
"PRAGMA table_info(localization_approvals)"
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
if columns != _RELEASE._COLUMNS:
|
|
339
|
+
reasons.add("release.schema_invalid")
|
|
340
|
+
except Exception:
|
|
341
|
+
reasons.add("release.schema_invalid")
|
|
342
|
+
try:
|
|
343
|
+
self.bridge._verify_schema()
|
|
344
|
+
except Exception:
|
|
345
|
+
reasons.add("cms.schema_invalid")
|
|
346
|
+
if self.evidence_state is not None:
|
|
347
|
+
try:
|
|
348
|
+
columns = tuple(
|
|
349
|
+
row["name"]
|
|
350
|
+
for row in self.evidence_state.connection.execute(
|
|
351
|
+
"PRAGMA table_info(localization_quality_evidence_state)"
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
if columns != _COORDINATOR._EVIDENCE_COLUMNS:
|
|
355
|
+
reasons.add("evidence.schema_invalid")
|
|
356
|
+
except Exception:
|
|
357
|
+
reasons.add("evidence.schema_invalid")
|
|
358
|
+
if self.benchmark_store is not None:
|
|
359
|
+
try:
|
|
360
|
+
self.benchmark_store._verify_schema()
|
|
361
|
+
except Exception:
|
|
362
|
+
reasons.add("benchmark.campaign.schema_invalid")
|
|
363
|
+
if self.benchmark_review_store is not None:
|
|
364
|
+
try:
|
|
365
|
+
self.benchmark_review_store._verify_schema()
|
|
366
|
+
except Exception:
|
|
367
|
+
reasons.add("review.store.schema_unsupported")
|
|
368
|
+
connections = [
|
|
369
|
+
("queue", self.queue.connection),
|
|
370
|
+
("release", self.release_store.connection),
|
|
371
|
+
("cms", self.bridge.connection),
|
|
372
|
+
]
|
|
373
|
+
if self.evidence_state is not None:
|
|
374
|
+
connections.append(("evidence", self.evidence_state.connection))
|
|
375
|
+
if self.benchmark_store is not None:
|
|
376
|
+
connections.append(("benchmark", self.benchmark_store.connection))
|
|
377
|
+
if self.benchmark_review_store is not None:
|
|
378
|
+
connections.append(("benchmark_review", self.benchmark_review_store.connection))
|
|
379
|
+
for name, connection in connections:
|
|
380
|
+
try:
|
|
381
|
+
if not self._quick_check(connection):
|
|
382
|
+
reasons.add(f"{name}.database_invalid")
|
|
383
|
+
except Exception:
|
|
384
|
+
reasons.add(f"{name}.database_invalid")
|
|
385
|
+
return reasons
|
|
386
|
+
|
|
387
|
+
def _check_queue(self, now: float) -> tuple[dict[str, int], set[str]]:
|
|
388
|
+
reasons: set[str] = set()
|
|
389
|
+
counts = _counts(self.queue.connection, "localization_jobs", QUEUE_STATUSES)
|
|
390
|
+
rows = self.queue.connection.execute("SELECT * FROM localization_jobs").fetchall()
|
|
391
|
+
for row in rows:
|
|
392
|
+
try:
|
|
393
|
+
if _hash(row["payload_json"]) != row["payload_sha256"]:
|
|
394
|
+
raise ValueError
|
|
395
|
+
payload = json.loads(row["payload_json"])
|
|
396
|
+
if _canonical_json(payload) != row["payload_json"]:
|
|
397
|
+
raise ValueError
|
|
398
|
+
if payload.get("job_id") != row["job_id"]:
|
|
399
|
+
raise ValueError
|
|
400
|
+
if payload.get("target", {}).get("locale") != row["target_locale"]:
|
|
401
|
+
raise ValueError
|
|
402
|
+
if row["status"] == "succeeded":
|
|
403
|
+
self.queue.result(row["job_id"])
|
|
404
|
+
if row["status"] == "leased" and float(row["lease_expires_at"]) <= now:
|
|
405
|
+
reasons.add("queue.lease_expired")
|
|
406
|
+
if row["status"] in {"retry_wait", "failed"}:
|
|
407
|
+
code = row["last_error_code"]
|
|
408
|
+
if not isinstance(code, str) or _QUEUE.ERROR_CODE.fullmatch(code) is None:
|
|
409
|
+
raise ValueError
|
|
410
|
+
reasons.add("queue.error." + code)
|
|
411
|
+
except Exception:
|
|
412
|
+
reasons.add("queue.state_invalid")
|
|
413
|
+
if counts["failed"]:
|
|
414
|
+
reasons.add("queue.locale_failed")
|
|
415
|
+
return counts, reasons
|
|
416
|
+
|
|
417
|
+
def _check_evidence(
|
|
418
|
+
self,
|
|
419
|
+
event_verifier: Any,
|
|
420
|
+
now: float,
|
|
421
|
+
) -> tuple[dict[str, int], set[str]]:
|
|
422
|
+
counts = {status: 0 for status in EVIDENCE_STATUSES}
|
|
423
|
+
reasons: set[str] = set()
|
|
424
|
+
if self.evidence_state is None:
|
|
425
|
+
return counts, reasons
|
|
426
|
+
connection = self.evidence_state.connection
|
|
427
|
+
counts = _counts(
|
|
428
|
+
connection,
|
|
429
|
+
"localization_quality_evidence_state",
|
|
430
|
+
EVIDENCE_STATUSES,
|
|
431
|
+
)
|
|
432
|
+
event_cache: dict[str, tuple[dict[str, Any], Any]] = {}
|
|
433
|
+
rows = connection.execute(
|
|
434
|
+
"SELECT * FROM localization_quality_evidence_state"
|
|
435
|
+
).fetchall()
|
|
436
|
+
for row in rows:
|
|
437
|
+
try:
|
|
438
|
+
state = _COORDINATOR.QualityEvidenceStateStore._status_from_row(row)
|
|
439
|
+
superseded = self.bridge.connection.execute(
|
|
440
|
+
"SELECT 1 FROM cms_event_supersessions WHERE event_id = ?",
|
|
441
|
+
(state.event_id,),
|
|
442
|
+
).fetchone() is not None
|
|
443
|
+
cancelled = self.bridge.connection.execute(
|
|
444
|
+
"SELECT 1 FROM cms_event_cancellations WHERE event_id = ?",
|
|
445
|
+
(state.event_id,),
|
|
446
|
+
).fetchone() is not None
|
|
447
|
+
if state.event_id not in event_cache:
|
|
448
|
+
event_cache[state.event_id] = self.bridge._load_event(
|
|
449
|
+
state.event_id,
|
|
450
|
+
event_verifier,
|
|
451
|
+
allow_superseded=superseded,
|
|
452
|
+
allow_cancelled=cancelled,
|
|
453
|
+
)
|
|
454
|
+
event, plan = event_cache[state.event_id]
|
|
455
|
+
if plan.plan_id != state.plan_id:
|
|
456
|
+
raise ValueError
|
|
457
|
+
job = next(
|
|
458
|
+
item for item in plan.jobs if item.job_id == state.job_id
|
|
459
|
+
)
|
|
460
|
+
result, result_sha256 = _COORDINATOR._validated_result(
|
|
461
|
+
self.release_store, plan, job,
|
|
462
|
+
)
|
|
463
|
+
if result_sha256 != state.result_sha256:
|
|
464
|
+
raise ValueError
|
|
465
|
+
expected = _COORDINATOR._request(
|
|
466
|
+
event,
|
|
467
|
+
plan,
|
|
468
|
+
job,
|
|
469
|
+
result,
|
|
470
|
+
result_sha256,
|
|
471
|
+
state.evidence_revision,
|
|
472
|
+
)
|
|
473
|
+
if expected.request_id != state.request_id:
|
|
474
|
+
raise ValueError
|
|
475
|
+
approval = self.release_store.connection.execute("""
|
|
476
|
+
SELECT 1 FROM localization_approvals
|
|
477
|
+
WHERE job_id = ? AND result_sha256 = ?
|
|
478
|
+
""", (state.job_id, state.result_sha256)).fetchone()
|
|
479
|
+
if state.status == "succeeded" and approval is None:
|
|
480
|
+
raise ValueError
|
|
481
|
+
if state.status != "succeeded" and approval is not None:
|
|
482
|
+
if not superseded:
|
|
483
|
+
reasons.add("evidence.approval_unreconciled")
|
|
484
|
+
if not superseded:
|
|
485
|
+
if state.status == "leased" and state.lease_expires_at <= now:
|
|
486
|
+
reasons.add("evidence.lease_expired")
|
|
487
|
+
if state.status in {"retry_wait", "failed"}:
|
|
488
|
+
reasons.add("evidence.error." + state.last_error_code)
|
|
489
|
+
if state.status == "failed":
|
|
490
|
+
reasons.add("evidence.review_failed")
|
|
491
|
+
except Exception:
|
|
492
|
+
reasons.add("evidence.state_invalid")
|
|
493
|
+
return counts, reasons
|
|
494
|
+
|
|
495
|
+
def _check_approvals(self, authority: Any, now: float) -> tuple[dict[str, int], set[str]]:
|
|
496
|
+
counts = {"total": 0, "current": 0, "expired": 0}
|
|
497
|
+
reasons: set[str] = set()
|
|
498
|
+
rows = self.release_store.connection.execute(
|
|
499
|
+
"SELECT * FROM localization_approvals"
|
|
500
|
+
).fetchall()
|
|
501
|
+
verify = getattr(authority, "verify", None)
|
|
502
|
+
for row in rows:
|
|
503
|
+
counts["total"] += 1
|
|
504
|
+
try:
|
|
505
|
+
if _hash(row["result_json"]) != row["result_sha256"]:
|
|
506
|
+
raise ValueError
|
|
507
|
+
if _hash(row["approval_json"]) != row["approval_sha256"]:
|
|
508
|
+
raise ValueError
|
|
509
|
+
result = json.loads(row["result_json"])
|
|
510
|
+
approval = json.loads(row["approval_json"])
|
|
511
|
+
if _canonical_json(result) != row["result_json"]:
|
|
512
|
+
raise ValueError
|
|
513
|
+
if _canonical_json(approval) != row["approval_json"]:
|
|
514
|
+
raise ValueError
|
|
515
|
+
if not isinstance(result, dict) or not isinstance(approval, dict):
|
|
516
|
+
raise ValueError
|
|
517
|
+
candidate = result.get("candidate")
|
|
518
|
+
if not isinstance(candidate, str) or _hash(candidate) != result.get("target_sha256"):
|
|
519
|
+
raise ValueError
|
|
520
|
+
result_binding = {
|
|
521
|
+
"job_id": row["job_id"],
|
|
522
|
+
"target_locale": row["target_locale"],
|
|
523
|
+
"target_sha256": row["target_sha256"],
|
|
524
|
+
}
|
|
525
|
+
if any(result.get(key) != value for key, value in result_binding.items()):
|
|
526
|
+
raise ValueError
|
|
527
|
+
approval_binding = {
|
|
528
|
+
"approval_id": row["approval_id"],
|
|
529
|
+
"job_id": row["job_id"],
|
|
530
|
+
"target_locale": row["target_locale"],
|
|
531
|
+
"target_sha256": row["target_sha256"],
|
|
532
|
+
"result_sha256": row["result_sha256"],
|
|
533
|
+
"approved_at": row["approved_at"],
|
|
534
|
+
"expires_at": row["expires_at"],
|
|
535
|
+
}
|
|
536
|
+
if any(approval.get(key) != value for key, value in approval_binding.items()):
|
|
537
|
+
raise ValueError
|
|
538
|
+
for key in (
|
|
539
|
+
"source_sha256", "target_sha256", "source_locale", "target_locale",
|
|
540
|
+
"content_type", "glossary_version", "policy_version", "provider",
|
|
541
|
+
"software_version", "worker_schema",
|
|
542
|
+
):
|
|
543
|
+
if approval.get(key) != result.get(key):
|
|
544
|
+
raise ValueError
|
|
545
|
+
immutable = {
|
|
546
|
+
key: value
|
|
547
|
+
for key, value in approval.items()
|
|
548
|
+
if key not in {"approval_id", "approved_at", "expires_at"}
|
|
549
|
+
}
|
|
550
|
+
if approval["approval_id"] != "blun-l10n-approval-" + _hash(
|
|
551
|
+
_canonical_json(immutable)
|
|
552
|
+
):
|
|
553
|
+
raise ValueError
|
|
554
|
+
signature = _RELEASE._signature(_RELEASE.ApprovalSignature(
|
|
555
|
+
row["signature_algorithm"], row["key_id"], row["signature"],
|
|
556
|
+
))
|
|
557
|
+
if not callable(verify) or verify(
|
|
558
|
+
row["approval_json"].encode("utf-8"), signature,
|
|
559
|
+
) is not True:
|
|
560
|
+
raise ValueError
|
|
561
|
+
except Exception:
|
|
562
|
+
reasons.add("release.approval_invalid")
|
|
563
|
+
continue
|
|
564
|
+
if float(row["expires_at"]) <= now:
|
|
565
|
+
counts["expired"] += 1
|
|
566
|
+
else:
|
|
567
|
+
counts["current"] += 1
|
|
568
|
+
return counts, reasons
|
|
569
|
+
|
|
570
|
+
def _check_deliveries(
|
|
571
|
+
self,
|
|
572
|
+
authority: Any,
|
|
573
|
+
now: float,
|
|
574
|
+
) -> tuple[dict[str, int], set[str]]:
|
|
575
|
+
counts = _counts(
|
|
576
|
+
self.bridge.connection,
|
|
577
|
+
"cms_publication_deliveries",
|
|
578
|
+
DELIVERY_STATUSES,
|
|
579
|
+
)
|
|
580
|
+
counts["superseded"] = 0
|
|
581
|
+
counts["cancelled"] = 0
|
|
582
|
+
reasons: set[str] = set()
|
|
583
|
+
verify = getattr(authority, "verify", None)
|
|
584
|
+
rows = self.bridge.connection.execute(
|
|
585
|
+
"SELECT * FROM cms_publication_deliveries"
|
|
586
|
+
).fetchall()
|
|
587
|
+
for row in rows:
|
|
588
|
+
try:
|
|
589
|
+
superseded = self.bridge.connection.execute(
|
|
590
|
+
"SELECT 1 FROM cms_event_supersessions WHERE event_id = ?",
|
|
591
|
+
(row["event_id"],),
|
|
592
|
+
).fetchone() is not None
|
|
593
|
+
cancelled = self.bridge.connection.execute(
|
|
594
|
+
"SELECT 1 FROM cms_event_cancellations WHERE event_id = ?",
|
|
595
|
+
(row["event_id"],),
|
|
596
|
+
).fetchone() is not None
|
|
597
|
+
if superseded and not cancelled and (
|
|
598
|
+
row["status"] != "failed" or row["last_error_code"] != "event_superseded"
|
|
599
|
+
):
|
|
600
|
+
raise ValueError
|
|
601
|
+
if cancelled and (
|
|
602
|
+
row["status"] != "failed" or row["last_error_code"] != "event_cancelled"
|
|
603
|
+
):
|
|
604
|
+
raise ValueError
|
|
605
|
+
if _hash(row["payload_json"]) != row["payload_sha256"]:
|
|
606
|
+
raise ValueError
|
|
607
|
+
payload = json.loads(row["payload_json"])
|
|
608
|
+
if _canonical_json(payload) != row["payload_json"]:
|
|
609
|
+
raise ValueError
|
|
610
|
+
if not isinstance(payload, dict) or set(payload) != {
|
|
611
|
+
"schema", "delivery_id", "event_id", "site_id", "website_version",
|
|
612
|
+
"plan_id", "source_id", "source_revision", "source_sequence",
|
|
613
|
+
"source_sha256",
|
|
614
|
+
"localizations",
|
|
615
|
+
}:
|
|
616
|
+
raise ValueError
|
|
617
|
+
if payload.get("schema") != _CMS.PUBLICATION_SCHEMA:
|
|
618
|
+
raise ValueError
|
|
619
|
+
if payload.get("delivery_id") != row["delivery_id"]:
|
|
620
|
+
raise ValueError
|
|
621
|
+
if payload.get("event_id") != row["event_id"] or payload.get("plan_id") != row["plan_id"]:
|
|
622
|
+
raise ValueError
|
|
623
|
+
unsigned = {key: value for key, value in payload.items() if key != "delivery_id"}
|
|
624
|
+
if payload["delivery_id"] != "blun-cms-delivery-" + _hash(
|
|
625
|
+
_canonical_json(unsigned)
|
|
626
|
+
):
|
|
627
|
+
raise ValueError
|
|
628
|
+
localizations = payload.get("localizations")
|
|
629
|
+
if not isinstance(localizations, list) or not localizations:
|
|
630
|
+
raise ValueError
|
|
631
|
+
locales: list[str] = []
|
|
632
|
+
for item in localizations:
|
|
633
|
+
if not isinstance(item, dict) or set(item) != {
|
|
634
|
+
"locale", "target_text", "target_sha256", "approval_id",
|
|
635
|
+
"approval_expires_at", "release_evidence",
|
|
636
|
+
}:
|
|
637
|
+
raise ValueError
|
|
638
|
+
if not isinstance(item["locale"], str) or not isinstance(item["target_text"], str):
|
|
639
|
+
raise ValueError
|
|
640
|
+
if _hash(item["target_text"]) != item["target_sha256"]:
|
|
641
|
+
raise ValueError
|
|
642
|
+
if not _CMS._valid_release_evidence(
|
|
643
|
+
item["release_evidence"],
|
|
644
|
+
locale=item["locale"],
|
|
645
|
+
target_sha256=item["target_sha256"],
|
|
646
|
+
approval_id=item["approval_id"],
|
|
647
|
+
):
|
|
648
|
+
raise ValueError
|
|
649
|
+
locales.append(item["locale"])
|
|
650
|
+
if locales != sorted(set(locales)):
|
|
651
|
+
raise ValueError
|
|
652
|
+
signature = _CMS._signature(_CMS.CMSMessageSignature(
|
|
653
|
+
row["signature_algorithm"], row["key_id"], row["signature"],
|
|
654
|
+
))
|
|
655
|
+
if not callable(verify) or verify(
|
|
656
|
+
row["payload_json"].encode("utf-8"), signature,
|
|
657
|
+
) is not True:
|
|
658
|
+
raise ValueError
|
|
659
|
+
if row["status"] != "succeeded":
|
|
660
|
+
if any(
|
|
661
|
+
not isinstance(item.get("approval_expires_at"), (int, float))
|
|
662
|
+
or isinstance(item.get("approval_expires_at"), bool)
|
|
663
|
+
or float(item["approval_expires_at"]) <= now
|
|
664
|
+
for item in localizations
|
|
665
|
+
):
|
|
666
|
+
reasons.add("cms.delivery.approval_expired")
|
|
667
|
+
if row["status"] == "leased" and float(row["lease_expires_at"]) <= now:
|
|
668
|
+
reasons.add("cms.delivery.lease_expired")
|
|
669
|
+
if row["status"] in {"retry_wait", "failed"}:
|
|
670
|
+
code = row["last_error_code"]
|
|
671
|
+
if not isinstance(code, str) or _CMS.ERROR_CODE.fullmatch(code) is None:
|
|
672
|
+
raise ValueError
|
|
673
|
+
if superseded and not cancelled and code == "event_superseded":
|
|
674
|
+
counts["failed"] -= 1
|
|
675
|
+
counts["superseded"] += 1
|
|
676
|
+
elif cancelled and code == "event_cancelled":
|
|
677
|
+
counts["failed"] -= 1
|
|
678
|
+
counts["cancelled"] += 1
|
|
679
|
+
else:
|
|
680
|
+
reasons.add("cms.delivery.error." + code)
|
|
681
|
+
except Exception:
|
|
682
|
+
reasons.add("cms.delivery.invalid")
|
|
683
|
+
if counts["failed"]:
|
|
684
|
+
reasons.add("cms.delivery.failed")
|
|
685
|
+
return counts, reasons
|
|
686
|
+
|
|
687
|
+
def _check_tombstones(
|
|
688
|
+
self,
|
|
689
|
+
event_verifier: Any,
|
|
690
|
+
publication_authority: Any,
|
|
691
|
+
now: float,
|
|
692
|
+
) -> tuple[dict[str, int], set[str]]:
|
|
693
|
+
counts = _counts(
|
|
694
|
+
self.bridge.connection,
|
|
695
|
+
"cms_tombstone_deliveries",
|
|
696
|
+
DELIVERY_STATUSES,
|
|
697
|
+
)
|
|
698
|
+
reasons: set[str] = set()
|
|
699
|
+
rows = self.bridge.connection.execute(
|
|
700
|
+
"SELECT * FROM cms_tombstone_deliveries"
|
|
701
|
+
).fetchall()
|
|
702
|
+
for row in rows:
|
|
703
|
+
try:
|
|
704
|
+
request = self.bridge._tombstone_request_from_row(
|
|
705
|
+
row, event_verifier, publication_authority,
|
|
706
|
+
)
|
|
707
|
+
status = self.bridge.tombstone_status(request.delivery_id)
|
|
708
|
+
if (
|
|
709
|
+
status.event_id != row["event_id"]
|
|
710
|
+
or status.plan_id != row["plan_id"]
|
|
711
|
+
or status.payload_sha256 != request.payload_sha256
|
|
712
|
+
or not 0 <= status.attempts <= status.max_attempts <= _CMS.MAX_ATTEMPTS
|
|
713
|
+
):
|
|
714
|
+
raise ValueError
|
|
715
|
+
if status.status == "leased" and (
|
|
716
|
+
status.lease_expires_at is None
|
|
717
|
+
or float(status.lease_expires_at) <= now
|
|
718
|
+
):
|
|
719
|
+
reasons.add("cms.tombstone.lease_expired")
|
|
720
|
+
if status.status in {"retry_wait", "failed"}:
|
|
721
|
+
if (
|
|
722
|
+
not isinstance(status.last_error_code, str)
|
|
723
|
+
or _CMS.ERROR_CODE.fullmatch(status.last_error_code) is None
|
|
724
|
+
):
|
|
725
|
+
raise ValueError
|
|
726
|
+
reasons.add("cms.tombstone.error." + status.last_error_code)
|
|
727
|
+
except Exception:
|
|
728
|
+
reasons.add("cms.tombstone.invalid")
|
|
729
|
+
if counts["failed"]:
|
|
730
|
+
reasons.add("cms.tombstone.failed")
|
|
731
|
+
return counts, reasons
|
|
732
|
+
|
|
733
|
+
def _check_supersessions(self, event_verifier: Any) -> set[str]:
|
|
734
|
+
reasons: set[str] = set()
|
|
735
|
+
topic_order: dict[tuple[str, str], list[tuple[int, float]]] = {}
|
|
736
|
+
topic_rows = self.bridge.connection.execute("""
|
|
737
|
+
SELECT event.event_id, event.created_at AS event_created_at,
|
|
738
|
+
topic.site_id, topic.source_id, topic.generation,
|
|
739
|
+
topic.created_at AS topic_created_at
|
|
740
|
+
FROM cms_change_events AS event
|
|
741
|
+
LEFT JOIN cms_event_topics AS topic ON topic.event_id = event.event_id
|
|
742
|
+
ORDER BY event.event_id
|
|
743
|
+
""").fetchall()
|
|
744
|
+
for row in topic_rows:
|
|
745
|
+
try:
|
|
746
|
+
event, _ = self.bridge._load_event(
|
|
747
|
+
row["event_id"],
|
|
748
|
+
event_verifier,
|
|
749
|
+
allow_superseded=True,
|
|
750
|
+
allow_cancelled=True,
|
|
751
|
+
allow_accepted=True,
|
|
752
|
+
)
|
|
753
|
+
if (
|
|
754
|
+
row["site_id"] != event["site_id"]
|
|
755
|
+
or row["source_id"] != event["localization"]["source_id"]
|
|
756
|
+
or isinstance(row["generation"], bool)
|
|
757
|
+
or not isinstance(row["generation"], int)
|
|
758
|
+
or row["generation"] <= 0
|
|
759
|
+
or row["topic_created_at"] != row["event_created_at"]
|
|
760
|
+
or (
|
|
761
|
+
"source_sequence" in event
|
|
762
|
+
and row["generation"] != event["source_sequence"]
|
|
763
|
+
)
|
|
764
|
+
):
|
|
765
|
+
raise ValueError
|
|
766
|
+
if "source_sequence" not in event:
|
|
767
|
+
topic_order.setdefault(
|
|
768
|
+
(row["site_id"], row["source_id"]), [],
|
|
769
|
+
).append((int(row["generation"]), float(row["topic_created_at"])))
|
|
770
|
+
except Exception:
|
|
771
|
+
reasons.add("cms.supersession.invalid")
|
|
772
|
+
for values in topic_order.values():
|
|
773
|
+
ordered = sorted(values)
|
|
774
|
+
if [generation for generation, _ in ordered] != list(
|
|
775
|
+
range(1, len(ordered) + 1)
|
|
776
|
+
) or [created_at for _, created_at in ordered] != sorted(
|
|
777
|
+
created_at for _, created_at in ordered
|
|
778
|
+
):
|
|
779
|
+
reasons.add("cms.supersession.invalid")
|
|
780
|
+
orphan_topics = self.bridge.connection.execute("""
|
|
781
|
+
SELECT COUNT(*)
|
|
782
|
+
FROM cms_event_topics AS topic
|
|
783
|
+
LEFT JOIN cms_change_events AS event ON event.event_id = topic.event_id
|
|
784
|
+
WHERE event.event_id IS NULL
|
|
785
|
+
""").fetchone()[0]
|
|
786
|
+
missing_supersessions = self.bridge.connection.execute("""
|
|
787
|
+
SELECT COUNT(*)
|
|
788
|
+
FROM cms_event_topics AS older
|
|
789
|
+
JOIN cms_change_events AS older_event ON older_event.event_id = older.event_id
|
|
790
|
+
LEFT JOIN cms_publication_deliveries AS delivery
|
|
791
|
+
ON delivery.event_id = older.event_id AND delivery.status = 'succeeded'
|
|
792
|
+
WHERE older_event.status = 'enqueued' AND delivery.event_id IS NULL
|
|
793
|
+
AND EXISTS (
|
|
794
|
+
SELECT 1
|
|
795
|
+
FROM cms_event_topics AS newer
|
|
796
|
+
JOIN cms_change_events AS newer_event
|
|
797
|
+
ON newer_event.event_id = newer.event_id
|
|
798
|
+
WHERE newer.site_id = older.site_id
|
|
799
|
+
AND newer.source_id = older.source_id
|
|
800
|
+
AND newer.generation > older.generation
|
|
801
|
+
AND newer_event.status = 'enqueued'
|
|
802
|
+
)
|
|
803
|
+
AND NOT EXISTS (
|
|
804
|
+
SELECT 1 FROM cms_event_supersessions AS supersession
|
|
805
|
+
WHERE supersession.event_id = older.event_id
|
|
806
|
+
)
|
|
807
|
+
""").fetchone()[0]
|
|
808
|
+
if orphan_topics or missing_supersessions:
|
|
809
|
+
reasons.add("cms.supersession.invalid")
|
|
810
|
+
rows = self.bridge.connection.execute("""
|
|
811
|
+
SELECT supersession.event_id, supersession.superseded_by_event_id,
|
|
812
|
+
older.site_id AS older_site_id, older.source_id AS older_source_id,
|
|
813
|
+
older.generation AS older_generation,
|
|
814
|
+
newer.site_id AS newer_site_id, newer.source_id AS newer_source_id,
|
|
815
|
+
newer.generation AS newer_generation,
|
|
816
|
+
delivery.status AS delivery_status
|
|
817
|
+
FROM cms_event_supersessions AS supersession
|
|
818
|
+
LEFT JOIN cms_event_topics AS older ON older.event_id = supersession.event_id
|
|
819
|
+
LEFT JOIN cms_event_topics AS newer
|
|
820
|
+
ON newer.event_id = supersession.superseded_by_event_id
|
|
821
|
+
LEFT JOIN cms_publication_deliveries AS delivery
|
|
822
|
+
ON delivery.event_id = supersession.event_id
|
|
823
|
+
ORDER BY supersession.event_id
|
|
824
|
+
""").fetchall()
|
|
825
|
+
for row in rows:
|
|
826
|
+
try:
|
|
827
|
+
if (
|
|
828
|
+
row["older_site_id"] is None
|
|
829
|
+
or row["newer_site_id"] is None
|
|
830
|
+
or row["older_site_id"] != row["newer_site_id"]
|
|
831
|
+
or row["older_source_id"] != row["newer_source_id"]
|
|
832
|
+
or int(row["older_generation"]) >= int(row["newer_generation"])
|
|
833
|
+
or row["delivery_status"] == "succeeded"
|
|
834
|
+
):
|
|
835
|
+
raise ValueError
|
|
836
|
+
self.bridge._load_event(
|
|
837
|
+
row["event_id"], event_verifier, allow_superseded=True,
|
|
838
|
+
allow_cancelled=True,
|
|
839
|
+
)
|
|
840
|
+
self.bridge._load_event(
|
|
841
|
+
row["superseded_by_event_id"],
|
|
842
|
+
event_verifier,
|
|
843
|
+
allow_superseded=True,
|
|
844
|
+
allow_cancelled=True,
|
|
845
|
+
)
|
|
846
|
+
except Exception:
|
|
847
|
+
reasons.add("cms.supersession.invalid")
|
|
848
|
+
cancellation_rows = self.bridge.connection.execute("""
|
|
849
|
+
SELECT cancellation.*, delivery.status AS delivery_status
|
|
850
|
+
FROM cms_event_cancellations AS cancellation
|
|
851
|
+
LEFT JOIN cms_publication_deliveries AS delivery
|
|
852
|
+
ON delivery.event_id = cancellation.event_id
|
|
853
|
+
ORDER BY cancellation.cancellation_id
|
|
854
|
+
""").fetchall()
|
|
855
|
+
for row in cancellation_rows:
|
|
856
|
+
try:
|
|
857
|
+
if row["delivery_status"] == "succeeded":
|
|
858
|
+
raise ValueError
|
|
859
|
+
self.bridge._load_event(
|
|
860
|
+
row["event_id"],
|
|
861
|
+
event_verifier,
|
|
862
|
+
allow_superseded=True,
|
|
863
|
+
allow_cancelled=True,
|
|
864
|
+
allow_accepted=True,
|
|
865
|
+
)
|
|
866
|
+
except Exception:
|
|
867
|
+
reasons.add("cms.cancellation.invalid")
|
|
868
|
+
return reasons
|
|
869
|
+
|
|
870
|
+
def _versions(
|
|
871
|
+
self,
|
|
872
|
+
event_verifier: Any,
|
|
873
|
+
approval_authority: Any,
|
|
874
|
+
now: float,
|
|
875
|
+
) -> tuple[tuple[WebsiteVersionHealth, ...], set[str], set[tuple[str, str, str]]]:
|
|
876
|
+
versions: list[WebsiteVersionHealth] = []
|
|
877
|
+
reasons: set[str] = set()
|
|
878
|
+
providers: set[tuple[str, str, str]] = set()
|
|
879
|
+
rows = self.bridge.connection.execute(
|
|
880
|
+
"SELECT event_id, status FROM cms_change_events ORDER BY event_id"
|
|
881
|
+
).fetchall()
|
|
882
|
+
for row in rows:
|
|
883
|
+
cancelled = self.bridge.connection.execute(
|
|
884
|
+
"SELECT 1 FROM cms_event_cancellations WHERE event_id = ?",
|
|
885
|
+
(row["event_id"],),
|
|
886
|
+
).fetchone() is not None
|
|
887
|
+
tombstone = self.bridge.connection.execute(
|
|
888
|
+
"SELECT status FROM cms_tombstone_deliveries WHERE event_id = ?",
|
|
889
|
+
(row["event_id"],),
|
|
890
|
+
).fetchone()
|
|
891
|
+
if row["status"] != "enqueued" and not cancelled:
|
|
892
|
+
reasons.add("cms.event.awaiting_queue_resume")
|
|
893
|
+
continue
|
|
894
|
+
try:
|
|
895
|
+
superseded = self.bridge.connection.execute(
|
|
896
|
+
"SELECT 1 FROM cms_event_supersessions WHERE event_id = ?",
|
|
897
|
+
(row["event_id"],),
|
|
898
|
+
).fetchone() is not None
|
|
899
|
+
event, plan = self.bridge._load_event(
|
|
900
|
+
row["event_id"], event_verifier,
|
|
901
|
+
allow_superseded=superseded,
|
|
902
|
+
allow_cancelled=cancelled,
|
|
903
|
+
allow_accepted=cancelled,
|
|
904
|
+
)
|
|
905
|
+
localization = event["localization"]
|
|
906
|
+
if not superseded and not cancelled and tombstone is None:
|
|
907
|
+
providers.add((
|
|
908
|
+
localization["provider_id"],
|
|
909
|
+
localization["model_id"],
|
|
910
|
+
localization["model_version"],
|
|
911
|
+
))
|
|
912
|
+
if cancelled and row["status"] == "accepted":
|
|
913
|
+
queue_counts = {status: 0 for status in QUEUE_STATUSES}
|
|
914
|
+
queue_counts["cancelled"] = len(plan.jobs)
|
|
915
|
+
else:
|
|
916
|
+
queue_counts = self.queue.plan_counts(plan.plan_id)
|
|
917
|
+
readiness = self.release_store.readiness(
|
|
918
|
+
plan, approval_authority, now=now,
|
|
919
|
+
)
|
|
920
|
+
except Exception:
|
|
921
|
+
reasons.add("cms.event.invalid")
|
|
922
|
+
continue
|
|
923
|
+
delivery = self.bridge.connection.execute(
|
|
924
|
+
"SELECT status FROM cms_publication_deliveries WHERE event_id = ?",
|
|
925
|
+
(event["event_id"],),
|
|
926
|
+
).fetchone()
|
|
927
|
+
delivery_status = delivery["status"] if delivery is not None else None
|
|
928
|
+
if cancelled:
|
|
929
|
+
status = "cancelled"
|
|
930
|
+
elif tombstone is not None and tombstone["status"] == "succeeded":
|
|
931
|
+
status = "deleted"
|
|
932
|
+
elif tombstone is not None and tombstone["status"] == "failed":
|
|
933
|
+
status = "deletion_failed"
|
|
934
|
+
elif tombstone is not None:
|
|
935
|
+
status = "deleting"
|
|
936
|
+
elif superseded:
|
|
937
|
+
status = "superseded"
|
|
938
|
+
elif delivery_status == "succeeded":
|
|
939
|
+
status = "published"
|
|
940
|
+
elif delivery_status == "failed":
|
|
941
|
+
status = "publication_failed"
|
|
942
|
+
elif delivery_status in {"pending", "leased", "retry_wait"}:
|
|
943
|
+
status = "publishing"
|
|
944
|
+
elif readiness.ready:
|
|
945
|
+
status = "ready"
|
|
946
|
+
elif queue_counts["failed"]:
|
|
947
|
+
status = "localization_failed"
|
|
948
|
+
elif queue_counts["succeeded"] == len(plan.jobs):
|
|
949
|
+
status = "awaiting_approval"
|
|
950
|
+
else:
|
|
951
|
+
status = "processing"
|
|
952
|
+
versions.append(WebsiteVersionHealth(
|
|
953
|
+
event_id=event["event_id"],
|
|
954
|
+
site_id=event["site_id"],
|
|
955
|
+
website_version=event["website_version"],
|
|
956
|
+
plan_id=plan.plan_id,
|
|
957
|
+
status=status,
|
|
958
|
+
required_locales=len(readiness.required_locales),
|
|
959
|
+
approved_locales=len(readiness.approved_locales),
|
|
960
|
+
queue_counts=tuple(sorted(queue_counts.items())),
|
|
961
|
+
blocked_locales=readiness.blocked,
|
|
962
|
+
))
|
|
963
|
+
if any(code == "approval.expired" for _, code in readiness.blocked):
|
|
964
|
+
reasons.add("release.approval_expired")
|
|
965
|
+
return tuple(versions), reasons, providers
|
|
966
|
+
|
|
967
|
+
@staticmethod
|
|
968
|
+
def _providers(
|
|
969
|
+
provider_bindings: set[tuple[str, str, str]],
|
|
970
|
+
probe: ProviderHealthProbe | None,
|
|
971
|
+
) -> tuple[tuple[ProviderHealth, ...], set[str]]:
|
|
972
|
+
statuses: list[ProviderHealth] = []
|
|
973
|
+
reasons: set[str] = set()
|
|
974
|
+
check = getattr(probe, "check", None)
|
|
975
|
+
for provider_id, model_id, model_version in sorted(provider_bindings):
|
|
976
|
+
reason = None
|
|
977
|
+
try:
|
|
978
|
+
if not callable(check):
|
|
979
|
+
raise ValueError("missing")
|
|
980
|
+
response = check(
|
|
981
|
+
provider_id=provider_id,
|
|
982
|
+
model_id=model_id,
|
|
983
|
+
model_version=model_version,
|
|
984
|
+
)
|
|
985
|
+
expected = {
|
|
986
|
+
"schema": PROVIDER_HEALTH_SCHEMA,
|
|
987
|
+
"provider": {
|
|
988
|
+
"id": provider_id,
|
|
989
|
+
"model_id": model_id,
|
|
990
|
+
"model_version": model_version,
|
|
991
|
+
},
|
|
992
|
+
"status": "healthy",
|
|
993
|
+
}
|
|
994
|
+
if not isinstance(response, Mapping) or dict(response) != expected:
|
|
995
|
+
raise ValueError("invalid")
|
|
996
|
+
status = "healthy"
|
|
997
|
+
except Exception:
|
|
998
|
+
status = "blocked"
|
|
999
|
+
reason = "provider.probe_missing" if not callable(check) else "provider.unavailable"
|
|
1000
|
+
reasons.add(reason)
|
|
1001
|
+
statuses.append(ProviderHealth(
|
|
1002
|
+
provider_id, model_id, model_version, status, reason,
|
|
1003
|
+
))
|
|
1004
|
+
return tuple(statuses), reasons
|
|
1005
|
+
|
|
1006
|
+
def _publisher(self, probe: PublisherHealthProbe | None) -> ComponentHealth | None:
|
|
1007
|
+
if probe is None:
|
|
1008
|
+
return None
|
|
1009
|
+
reasons: set[str] = set()
|
|
1010
|
+
try:
|
|
1011
|
+
check = getattr(probe, "check", None)
|
|
1012
|
+
if not callable(check):
|
|
1013
|
+
raise ValueError
|
|
1014
|
+
capabilities = self.bridge.localization_capabilities()
|
|
1015
|
+
contract = capabilities["publication_http"]
|
|
1016
|
+
contract_sha256 = contract["sha256"]
|
|
1017
|
+
response = check(contract_sha256=contract_sha256)
|
|
1018
|
+
if (
|
|
1019
|
+
not isinstance(response, Mapping)
|
|
1020
|
+
or set(response) != {
|
|
1021
|
+
"schema", "probe_id", "contract_sha256", "status",
|
|
1022
|
+
}
|
|
1023
|
+
or response["schema"] != _CMS.PUBLICATION_HEALTH_ACK_SCHEMA
|
|
1024
|
+
or not isinstance(response["probe_id"], str)
|
|
1025
|
+
or not 16 <= len(response["probe_id"]) <= 256
|
|
1026
|
+
or _CMS.TOKEN.fullmatch(response["probe_id"]) is None
|
|
1027
|
+
or response["contract_sha256"] != contract_sha256
|
|
1028
|
+
or response["status"] != "healthy"
|
|
1029
|
+
):
|
|
1030
|
+
raise ValueError
|
|
1031
|
+
except Exception:
|
|
1032
|
+
reasons.add("cms.publisher_unavailable")
|
|
1033
|
+
return _component(
|
|
1034
|
+
"cms_publisher",
|
|
1035
|
+
"blocked" if reasons else "healthy",
|
|
1036
|
+
reasons,
|
|
1037
|
+
{
|
|
1038
|
+
"configured": 1,
|
|
1039
|
+
"healthy": int(not reasons),
|
|
1040
|
+
"blocked": int(bool(reasons)),
|
|
1041
|
+
},
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
def _check_supervisor(self, now: float) -> ComponentHealth | None:
|
|
1045
|
+
if self.supervisor is None:
|
|
1046
|
+
return None
|
|
1047
|
+
counts = {"revision": 0, "consecutive_blocked": 0, "lease_active": 0}
|
|
1048
|
+
reasons: set[str] = set()
|
|
1049
|
+
status = "blocked"
|
|
1050
|
+
try:
|
|
1051
|
+
value = self.supervisor.status(now=now)
|
|
1052
|
+
payload_method = getattr(value, "as_payload", None)
|
|
1053
|
+
payload = payload_method() if callable(payload_method) else value
|
|
1054
|
+
expected = {
|
|
1055
|
+
"schema", "status", "revision", "lease_active",
|
|
1056
|
+
"lease_expires_at", "next_tick_at", "consecutive_blocked",
|
|
1057
|
+
"last_started_at", "last_finished_at", "last_phase",
|
|
1058
|
+
"last_status", "last_error_code",
|
|
1059
|
+
}
|
|
1060
|
+
if not isinstance(payload, Mapping) or set(payload) != expected:
|
|
1061
|
+
raise ValueError
|
|
1062
|
+
if payload["schema"] != SUPERVISOR_SCHEMA or payload["status"] not in {
|
|
1063
|
+
"ready", "waiting", "leased", "recoverable",
|
|
1064
|
+
}:
|
|
1065
|
+
raise ValueError
|
|
1066
|
+
if not isinstance(payload["lease_active"], bool):
|
|
1067
|
+
raise ValueError
|
|
1068
|
+
revision = payload["revision"]
|
|
1069
|
+
blocked = payload["consecutive_blocked"]
|
|
1070
|
+
if any(
|
|
1071
|
+
isinstance(item, bool) or not isinstance(item, int) or item < 0
|
|
1072
|
+
for item in (revision, blocked)
|
|
1073
|
+
):
|
|
1074
|
+
raise ValueError
|
|
1075
|
+
_timestamp(payload["next_tick_at"])
|
|
1076
|
+
for name in ("lease_expires_at", "last_started_at", "last_finished_at"):
|
|
1077
|
+
if payload[name] is not None:
|
|
1078
|
+
_timestamp(payload[name])
|
|
1079
|
+
error_code = payload["last_error_code"]
|
|
1080
|
+
if error_code is not None and (
|
|
1081
|
+
not isinstance(error_code, str) or _QUEUE.ERROR_CODE.fullmatch(error_code) is None
|
|
1082
|
+
):
|
|
1083
|
+
raise ValueError
|
|
1084
|
+
last_phase = payload["last_phase"]
|
|
1085
|
+
last_status = payload["last_status"]
|
|
1086
|
+
if (last_phase is None) != (last_status is None):
|
|
1087
|
+
raise ValueError
|
|
1088
|
+
if last_phase is not None and (
|
|
1089
|
+
last_phase not in SUPERVISOR_PHASES
|
|
1090
|
+
or last_status not in SUPERVISOR_STATUSES
|
|
1091
|
+
):
|
|
1092
|
+
raise ValueError
|
|
1093
|
+
blocked_status = last_status in {"blocked", "failed", "retry_wait"}
|
|
1094
|
+
if blocked_status != (error_code is not None):
|
|
1095
|
+
raise ValueError
|
|
1096
|
+
if (blocked > 0) != blocked_status:
|
|
1097
|
+
raise ValueError
|
|
1098
|
+
counts = {
|
|
1099
|
+
"revision": revision,
|
|
1100
|
+
"consecutive_blocked": blocked,
|
|
1101
|
+
"lease_active": int(payload["lease_active"]),
|
|
1102
|
+
}
|
|
1103
|
+
if payload["status"] == "recoverable":
|
|
1104
|
+
reasons.add("supervisor.lease_expired")
|
|
1105
|
+
if (
|
|
1106
|
+
payload["status"] == "ready"
|
|
1107
|
+
and now - float(payload["next_tick_at"])
|
|
1108
|
+
> self.supervisor_stale_after_seconds
|
|
1109
|
+
):
|
|
1110
|
+
reasons.add("supervisor.heartbeat_stale")
|
|
1111
|
+
if blocked_status:
|
|
1112
|
+
reasons.add("supervisor.last_error." + error_code)
|
|
1113
|
+
status = "degraded" if reasons else "healthy"
|
|
1114
|
+
except Exception:
|
|
1115
|
+
reasons = {"supervisor.state_invalid"}
|
|
1116
|
+
status = "blocked"
|
|
1117
|
+
return _component("supervisor", status, reasons, counts)
|
|
1118
|
+
|
|
1119
|
+
def _check_benchmark(self, now: float) -> ComponentHealth | None:
|
|
1120
|
+
if self.benchmark_store is None:
|
|
1121
|
+
return None
|
|
1122
|
+
counts = {status: 0 for status in _CAMPAIGN.STATUSES}
|
|
1123
|
+
counts.update({"work_count": 0, "report_ready": 0})
|
|
1124
|
+
try:
|
|
1125
|
+
value = self.benchmark_store.health(
|
|
1126
|
+
self.benchmark_policy,
|
|
1127
|
+
self.benchmark_campaign_id,
|
|
1128
|
+
self.benchmark_evidence_authority,
|
|
1129
|
+
now=now,
|
|
1130
|
+
stale_after_seconds=self.benchmark_stale_after_seconds,
|
|
1131
|
+
)
|
|
1132
|
+
payload_method = getattr(value, "as_payload", None)
|
|
1133
|
+
payload = payload_method() if callable(payload_method) else value
|
|
1134
|
+
expected = {
|
|
1135
|
+
"schema", "campaign_id", "status", "reasons", "counts",
|
|
1136
|
+
"work_count", "report_ready", "last_progress_at",
|
|
1137
|
+
}
|
|
1138
|
+
if (
|
|
1139
|
+
not isinstance(payload, Mapping)
|
|
1140
|
+
or set(payload) != expected
|
|
1141
|
+
or payload["schema"] != _CAMPAIGN.HEALTH_SCHEMA
|
|
1142
|
+
or payload["campaign_id"] != self.benchmark_campaign_id
|
|
1143
|
+
or payload["status"] not in {"healthy", "degraded", "blocked"}
|
|
1144
|
+
or not isinstance(payload["reasons"], list)
|
|
1145
|
+
or payload["reasons"] != sorted(set(payload["reasons"]))
|
|
1146
|
+
or not isinstance(payload["counts"], Mapping)
|
|
1147
|
+
or set(payload["counts"]) != set(_CAMPAIGN.STATUSES)
|
|
1148
|
+
or not isinstance(payload["report_ready"], bool)
|
|
1149
|
+
):
|
|
1150
|
+
raise ValueError
|
|
1151
|
+
work_count = payload["work_count"]
|
|
1152
|
+
if (
|
|
1153
|
+
isinstance(work_count, bool)
|
|
1154
|
+
or not isinstance(work_count, int)
|
|
1155
|
+
or work_count <= 0
|
|
1156
|
+
):
|
|
1157
|
+
raise ValueError
|
|
1158
|
+
for name, count in payload["counts"].items():
|
|
1159
|
+
if isinstance(count, bool) or not isinstance(count, int) or count < 0:
|
|
1160
|
+
raise ValueError
|
|
1161
|
+
counts[name] = count
|
|
1162
|
+
if sum(counts[name] for name in _CAMPAIGN.STATUSES) != work_count:
|
|
1163
|
+
raise ValueError
|
|
1164
|
+
reasons = set(payload["reasons"])
|
|
1165
|
+
if any(
|
|
1166
|
+
not isinstance(reason, str)
|
|
1167
|
+
or _CAMPAIGN.HEALTH_REASON.fullmatch(reason) is None
|
|
1168
|
+
or not reason.startswith("benchmark.campaign.")
|
|
1169
|
+
for reason in reasons
|
|
1170
|
+
):
|
|
1171
|
+
raise ValueError
|
|
1172
|
+
progress = payload["last_progress_at"]
|
|
1173
|
+
if progress is None or _timestamp(progress) > now:
|
|
1174
|
+
raise ValueError
|
|
1175
|
+
blocking_reasons = {
|
|
1176
|
+
"benchmark.campaign.failed",
|
|
1177
|
+
"benchmark.campaign.report_invalid",
|
|
1178
|
+
"benchmark.campaign.state_invalid",
|
|
1179
|
+
}
|
|
1180
|
+
expected_status = (
|
|
1181
|
+
"blocked" if counts["failed"] or reasons & blocking_reasons
|
|
1182
|
+
else "degraded" if reasons
|
|
1183
|
+
else "healthy"
|
|
1184
|
+
)
|
|
1185
|
+
if payload["status"] != expected_status:
|
|
1186
|
+
raise ValueError
|
|
1187
|
+
if counts["failed"] and "benchmark.campaign.failed" not in reasons:
|
|
1188
|
+
raise ValueError
|
|
1189
|
+
if payload["report_ready"] != (
|
|
1190
|
+
counts["succeeded"] == work_count
|
|
1191
|
+
and not reasons & {
|
|
1192
|
+
"benchmark.campaign.report_invalid",
|
|
1193
|
+
"benchmark.campaign.report_missing",
|
|
1194
|
+
}
|
|
1195
|
+
):
|
|
1196
|
+
raise ValueError
|
|
1197
|
+
counts["work_count"] = work_count
|
|
1198
|
+
counts["report_ready"] = int(payload["report_ready"])
|
|
1199
|
+
return _component(
|
|
1200
|
+
"benchmark_campaign", payload["status"], reasons, counts,
|
|
1201
|
+
)
|
|
1202
|
+
except Exception:
|
|
1203
|
+
return _component(
|
|
1204
|
+
"benchmark_campaign",
|
|
1205
|
+
"blocked",
|
|
1206
|
+
{"benchmark.campaign.state_invalid"},
|
|
1207
|
+
counts,
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
def _check_benchmark_reviews(self, now: float) -> ComponentHealth | None:
|
|
1211
|
+
if self.benchmark_review_store is None:
|
|
1212
|
+
return None
|
|
1213
|
+
counts = {
|
|
1214
|
+
"total": 0,
|
|
1215
|
+
"scoped": 0,
|
|
1216
|
+
"historical": 0,
|
|
1217
|
+
"target_native": 0,
|
|
1218
|
+
"source_fidelity": 0,
|
|
1219
|
+
"required": 0,
|
|
1220
|
+
"matched": 0,
|
|
1221
|
+
}
|
|
1222
|
+
try:
|
|
1223
|
+
rows = self.benchmark_store.connection.execute("""
|
|
1224
|
+
SELECT result_json, result_sha256
|
|
1225
|
+
FROM benchmark_campaign_work
|
|
1226
|
+
WHERE campaign_id = ? AND status = 'succeeded'
|
|
1227
|
+
ORDER BY target_locale, suite_case_key
|
|
1228
|
+
""", (self.benchmark_campaign_id,)).fetchall()
|
|
1229
|
+
expected_passes = []
|
|
1230
|
+
for row in rows:
|
|
1231
|
+
if (
|
|
1232
|
+
not isinstance(row["result_json"], str)
|
|
1233
|
+
or row["result_sha256"] != _hash(row["result_json"])
|
|
1234
|
+
):
|
|
1235
|
+
raise ValueError
|
|
1236
|
+
result = json.loads(row["result_json"])
|
|
1237
|
+
if _canonical_json(result) != row["result_json"]:
|
|
1238
|
+
raise ValueError
|
|
1239
|
+
validated = _CAMPAIGN._BENCHMARK._validated_case_result(
|
|
1240
|
+
result,
|
|
1241
|
+
self.benchmark_policy,
|
|
1242
|
+
self.benchmark_evidence_authority,
|
|
1243
|
+
)
|
|
1244
|
+
expected_passes.extend({
|
|
1245
|
+
"phase": item["phase"],
|
|
1246
|
+
"request_sha256": item["request_sha256"],
|
|
1247
|
+
"response_sha256": item["response_sha256"],
|
|
1248
|
+
} for item in validated["passes"])
|
|
1249
|
+
value = self.benchmark_review_store.health(
|
|
1250
|
+
self.benchmark_policy,
|
|
1251
|
+
self.benchmark_reviewer_route_id,
|
|
1252
|
+
evidence_authority=self.benchmark_evidence_authority,
|
|
1253
|
+
expected_passes=tuple(expected_passes),
|
|
1254
|
+
now=now,
|
|
1255
|
+
)
|
|
1256
|
+
payload_method = getattr(value, "as_payload", None)
|
|
1257
|
+
payload = payload_method() if callable(payload_method) else value
|
|
1258
|
+
expected = {"schema", "route_id", "status", "reasons", "counts"}
|
|
1259
|
+
if (
|
|
1260
|
+
not isinstance(payload, Mapping)
|
|
1261
|
+
or set(payload) != expected
|
|
1262
|
+
or payload["schema"] != _BENCHMARK_REVIEW.HEALTH_SCHEMA
|
|
1263
|
+
or payload["route_id"] != self.benchmark_reviewer_route_id
|
|
1264
|
+
or payload["status"] not in {"healthy", "blocked"}
|
|
1265
|
+
or not isinstance(payload["reasons"], list)
|
|
1266
|
+
or payload["reasons"] != sorted(set(payload["reasons"]))
|
|
1267
|
+
or not isinstance(payload["counts"], Mapping)
|
|
1268
|
+
or set(payload["counts"]) != set(counts)
|
|
1269
|
+
):
|
|
1270
|
+
raise ValueError
|
|
1271
|
+
for name, count in payload["counts"].items():
|
|
1272
|
+
if isinstance(count, bool) or not isinstance(count, int) or count < 0:
|
|
1273
|
+
raise ValueError
|
|
1274
|
+
counts[name] = count
|
|
1275
|
+
reasons = set(payload["reasons"])
|
|
1276
|
+
if any(
|
|
1277
|
+
not isinstance(reason, str)
|
|
1278
|
+
or _CAMPAIGN.HEALTH_REASON.fullmatch(reason) is None
|
|
1279
|
+
or not reason.startswith("review.store.")
|
|
1280
|
+
for reason in reasons
|
|
1281
|
+
):
|
|
1282
|
+
raise ValueError
|
|
1283
|
+
if counts["matched"] > counts["required"]:
|
|
1284
|
+
raise ValueError
|
|
1285
|
+
expected_status = "blocked" if reasons else "healthy"
|
|
1286
|
+
if payload["status"] != expected_status:
|
|
1287
|
+
raise ValueError
|
|
1288
|
+
return _component("benchmark_reviews", expected_status, reasons, counts)
|
|
1289
|
+
except Exception:
|
|
1290
|
+
return _component(
|
|
1291
|
+
"benchmark_reviews",
|
|
1292
|
+
"blocked",
|
|
1293
|
+
{"review.store.state_invalid"},
|
|
1294
|
+
counts,
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
def _check_native_reference_queue(
|
|
1298
|
+
self, now: float,
|
|
1299
|
+
) -> ComponentHealth | None:
|
|
1300
|
+
if self.benchmark_reference_queue is None:
|
|
1301
|
+
return None
|
|
1302
|
+
counts = {status: 0 for status in _REFERENCE_QUEUE.STATUSES}
|
|
1303
|
+
counts["work_count"] = 0
|
|
1304
|
+
try:
|
|
1305
|
+
value = self.benchmark_reference_queue.native_reference_queue_health(
|
|
1306
|
+
now=now,
|
|
1307
|
+
stale_after_seconds=self.benchmark_stale_after_seconds,
|
|
1308
|
+
)
|
|
1309
|
+
payload_method = getattr(value, "as_payload", None)
|
|
1310
|
+
payload = payload_method() if callable(payload_method) else value
|
|
1311
|
+
expected = {
|
|
1312
|
+
"schema", "campaign_id", "status", "reasons", "counts",
|
|
1313
|
+
"work_count", "last_progress_at",
|
|
1314
|
+
}
|
|
1315
|
+
if (
|
|
1316
|
+
not isinstance(payload, Mapping)
|
|
1317
|
+
or set(payload) != expected
|
|
1318
|
+
or payload["schema"] != _REFERENCE_QUEUE.HEALTH_SCHEMA
|
|
1319
|
+
or payload["campaign_id"] != self.benchmark_campaign_id
|
|
1320
|
+
or payload["status"] not in {"healthy", "degraded", "blocked"}
|
|
1321
|
+
or not isinstance(payload["reasons"], list)
|
|
1322
|
+
or payload["reasons"] != sorted(set(payload["reasons"]))
|
|
1323
|
+
or not isinstance(payload["counts"], Mapping)
|
|
1324
|
+
or set(payload["counts"]) != set(_REFERENCE_QUEUE.STATUSES)
|
|
1325
|
+
):
|
|
1326
|
+
raise ValueError
|
|
1327
|
+
work_count = payload["work_count"]
|
|
1328
|
+
if (
|
|
1329
|
+
isinstance(work_count, bool) or not isinstance(work_count, int)
|
|
1330
|
+
or work_count <= 0
|
|
1331
|
+
):
|
|
1332
|
+
raise ValueError
|
|
1333
|
+
for name, count in payload["counts"].items():
|
|
1334
|
+
if isinstance(count, bool) or not isinstance(count, int) or count < 0:
|
|
1335
|
+
raise ValueError
|
|
1336
|
+
counts[name] = count
|
|
1337
|
+
if sum(counts[name] for name in _REFERENCE_QUEUE.STATUSES) != work_count:
|
|
1338
|
+
raise ValueError
|
|
1339
|
+
reasons = set(payload["reasons"])
|
|
1340
|
+
if any(
|
|
1341
|
+
not isinstance(reason, str)
|
|
1342
|
+
or _CAMPAIGN.HEALTH_REASON.fullmatch(reason) is None
|
|
1343
|
+
or not reason.startswith("native_reference.queue.")
|
|
1344
|
+
for reason in reasons
|
|
1345
|
+
):
|
|
1346
|
+
raise ValueError
|
|
1347
|
+
progress = payload["last_progress_at"]
|
|
1348
|
+
if progress is None or _timestamp(progress) > now:
|
|
1349
|
+
raise ValueError
|
|
1350
|
+
expected_status = (
|
|
1351
|
+
"blocked" if counts["failed"] or any(
|
|
1352
|
+
reason in {
|
|
1353
|
+
"native_reference.queue.policy_expired",
|
|
1354
|
+
"native_reference.queue.failed",
|
|
1355
|
+
"native_reference.queue.state_invalid",
|
|
1356
|
+
} for reason in reasons
|
|
1357
|
+
)
|
|
1358
|
+
else "degraded" if reasons else "healthy"
|
|
1359
|
+
)
|
|
1360
|
+
if payload["status"] != expected_status:
|
|
1361
|
+
raise ValueError
|
|
1362
|
+
counts["work_count"] = work_count
|
|
1363
|
+
return _component(
|
|
1364
|
+
"benchmark_native_references",
|
|
1365
|
+
expected_status,
|
|
1366
|
+
reasons,
|
|
1367
|
+
counts,
|
|
1368
|
+
)
|
|
1369
|
+
except Exception:
|
|
1370
|
+
return _component(
|
|
1371
|
+
"benchmark_native_references",
|
|
1372
|
+
"blocked",
|
|
1373
|
+
{"native_reference.queue.state_invalid"},
|
|
1374
|
+
counts,
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
def check(
|
|
1378
|
+
self,
|
|
1379
|
+
*,
|
|
1380
|
+
event_verifier: Any,
|
|
1381
|
+
approval_authority: Any,
|
|
1382
|
+
publication_authority: Any,
|
|
1383
|
+
provider_probe: ProviderHealthProbe | None,
|
|
1384
|
+
publisher_probe: PublisherHealthProbe | None = None,
|
|
1385
|
+
now: float | int,
|
|
1386
|
+
) -> LocalizationHealthReport:
|
|
1387
|
+
now = _timestamp(now)
|
|
1388
|
+
storage_reasons = self._check_schemas()
|
|
1389
|
+
queue_counts: dict[str, int] = {status: 0 for status in QUEUE_STATUSES}
|
|
1390
|
+
approval_counts = {"total": 0, "current": 0, "expired": 0}
|
|
1391
|
+
delivery_counts: dict[str, int] = {status: 0 for status in DELIVERY_STATUSES}
|
|
1392
|
+
tombstone_counts: dict[str, int] = {status: 0 for status in DELIVERY_STATUSES}
|
|
1393
|
+
evidence_counts: dict[str, int] = {status: 0 for status in EVIDENCE_STATUSES}
|
|
1394
|
+
workflow_reasons: set[str] = set()
|
|
1395
|
+
versions: tuple[WebsiteVersionHealth, ...] = ()
|
|
1396
|
+
provider_bindings: set[tuple[str, str, str]] = set()
|
|
1397
|
+
|
|
1398
|
+
if not storage_reasons:
|
|
1399
|
+
try:
|
|
1400
|
+
queue_counts, queue_reasons = self._check_queue(now)
|
|
1401
|
+
evidence_counts, evidence_reasons = self._check_evidence(
|
|
1402
|
+
event_verifier, now,
|
|
1403
|
+
)
|
|
1404
|
+
approval_counts, approval_reasons = self._check_approvals(
|
|
1405
|
+
approval_authority, now,
|
|
1406
|
+
)
|
|
1407
|
+
delivery_counts, delivery_reasons = self._check_deliveries(
|
|
1408
|
+
publication_authority, now,
|
|
1409
|
+
)
|
|
1410
|
+
tombstone_counts, tombstone_reasons = self._check_tombstones(
|
|
1411
|
+
event_verifier, publication_authority, now,
|
|
1412
|
+
)
|
|
1413
|
+
supersession_reasons = self._check_supersessions(event_verifier)
|
|
1414
|
+
versions, event_reasons, provider_bindings = self._versions(
|
|
1415
|
+
event_verifier, approval_authority, now,
|
|
1416
|
+
)
|
|
1417
|
+
workflow_reasons.update(queue_reasons)
|
|
1418
|
+
workflow_reasons.update(evidence_reasons)
|
|
1419
|
+
workflow_reasons.update(approval_reasons)
|
|
1420
|
+
workflow_reasons.update(delivery_reasons)
|
|
1421
|
+
workflow_reasons.update(tombstone_reasons)
|
|
1422
|
+
workflow_reasons.update(supersession_reasons)
|
|
1423
|
+
workflow_reasons.update(event_reasons)
|
|
1424
|
+
except Exception:
|
|
1425
|
+
storage_reasons.add("monitor.state_unreadable")
|
|
1426
|
+
|
|
1427
|
+
providers, provider_reasons = self._providers(provider_bindings, provider_probe)
|
|
1428
|
+
publisher = self._publisher(publisher_probe)
|
|
1429
|
+
supervisor = self._check_supervisor(now)
|
|
1430
|
+
benchmark = self._check_benchmark(now)
|
|
1431
|
+
benchmark_reviews = self._check_benchmark_reviews(now)
|
|
1432
|
+
benchmark_references = self._check_native_reference_queue(now)
|
|
1433
|
+
blocking_workflow = {
|
|
1434
|
+
"queue.state_invalid",
|
|
1435
|
+
"evidence.state_invalid",
|
|
1436
|
+
"release.approval_invalid",
|
|
1437
|
+
"cms.delivery.invalid",
|
|
1438
|
+
"cms.tombstone.invalid",
|
|
1439
|
+
"cms.event.invalid",
|
|
1440
|
+
"cms.supersession.invalid",
|
|
1441
|
+
}
|
|
1442
|
+
queue_reasons = {reason for reason in workflow_reasons if reason.startswith("queue.")}
|
|
1443
|
+
evidence_reasons = {
|
|
1444
|
+
reason for reason in workflow_reasons if reason.startswith("evidence.")
|
|
1445
|
+
}
|
|
1446
|
+
release_reasons = {reason for reason in workflow_reasons if reason.startswith("release.")}
|
|
1447
|
+
cms_reasons = {reason for reason in workflow_reasons if reason.startswith("cms.")}
|
|
1448
|
+
components = (
|
|
1449
|
+
_component(
|
|
1450
|
+
"storage",
|
|
1451
|
+
"blocked" if storage_reasons else "healthy",
|
|
1452
|
+
storage_reasons,
|
|
1453
|
+
{"connections": 3 + int(self.evidence_state is not None) + int(
|
|
1454
|
+
self.benchmark_store is not None
|
|
1455
|
+
) + int(self.benchmark_review_store is not None)},
|
|
1456
|
+
),
|
|
1457
|
+
_component(
|
|
1458
|
+
"queue",
|
|
1459
|
+
"blocked" if queue_reasons & blocking_workflow else (
|
|
1460
|
+
"degraded" if queue_reasons else "healthy"
|
|
1461
|
+
),
|
|
1462
|
+
queue_reasons,
|
|
1463
|
+
queue_counts,
|
|
1464
|
+
),
|
|
1465
|
+
_component(
|
|
1466
|
+
"evidence",
|
|
1467
|
+
"blocked" if evidence_reasons & blocking_workflow else (
|
|
1468
|
+
"degraded" if evidence_reasons else "healthy"
|
|
1469
|
+
),
|
|
1470
|
+
evidence_reasons,
|
|
1471
|
+
evidence_counts,
|
|
1472
|
+
),
|
|
1473
|
+
_component(
|
|
1474
|
+
"release",
|
|
1475
|
+
"blocked" if release_reasons & blocking_workflow else (
|
|
1476
|
+
"degraded" if release_reasons else "healthy"
|
|
1477
|
+
),
|
|
1478
|
+
release_reasons,
|
|
1479
|
+
approval_counts,
|
|
1480
|
+
),
|
|
1481
|
+
_component(
|
|
1482
|
+
"cms",
|
|
1483
|
+
"blocked" if cms_reasons & blocking_workflow else (
|
|
1484
|
+
"degraded" if cms_reasons else "healthy"
|
|
1485
|
+
),
|
|
1486
|
+
cms_reasons,
|
|
1487
|
+
{
|
|
1488
|
+
**delivery_counts,
|
|
1489
|
+
**{f"tombstone_{key}": value for key, value in tombstone_counts.items()},
|
|
1490
|
+
},
|
|
1491
|
+
),
|
|
1492
|
+
_component(
|
|
1493
|
+
"providers",
|
|
1494
|
+
"blocked" if provider_reasons else "healthy",
|
|
1495
|
+
provider_reasons,
|
|
1496
|
+
{
|
|
1497
|
+
"configured": len(providers),
|
|
1498
|
+
"healthy": sum(item.status == "healthy" for item in providers),
|
|
1499
|
+
"blocked": sum(item.status == "blocked" for item in providers),
|
|
1500
|
+
},
|
|
1501
|
+
),
|
|
1502
|
+
)
|
|
1503
|
+
if supervisor is not None:
|
|
1504
|
+
components = components + (supervisor,)
|
|
1505
|
+
if publisher is not None:
|
|
1506
|
+
components = components + (publisher,)
|
|
1507
|
+
if benchmark is not None:
|
|
1508
|
+
components = components + (benchmark,)
|
|
1509
|
+
if benchmark_reviews is not None:
|
|
1510
|
+
components = components + (benchmark_reviews,)
|
|
1511
|
+
if benchmark_references is not None:
|
|
1512
|
+
components = components + (benchmark_references,)
|
|
1513
|
+
if (
|
|
1514
|
+
storage_reasons
|
|
1515
|
+
or provider_reasons
|
|
1516
|
+
or (publisher is not None and publisher.status == "blocked")
|
|
1517
|
+
or workflow_reasons & blocking_workflow
|
|
1518
|
+
or (supervisor is not None and supervisor.status == "blocked")
|
|
1519
|
+
or (benchmark is not None and benchmark.status == "blocked")
|
|
1520
|
+
or (
|
|
1521
|
+
benchmark_reviews is not None
|
|
1522
|
+
and benchmark_reviews.status == "blocked"
|
|
1523
|
+
)
|
|
1524
|
+
or (
|
|
1525
|
+
benchmark_references is not None
|
|
1526
|
+
and benchmark_references.status == "blocked"
|
|
1527
|
+
)
|
|
1528
|
+
):
|
|
1529
|
+
status = "blocked"
|
|
1530
|
+
elif workflow_reasons or (
|
|
1531
|
+
supervisor is not None and supervisor.status == "degraded"
|
|
1532
|
+
) or (
|
|
1533
|
+
benchmark is not None and benchmark.status == "degraded"
|
|
1534
|
+
) or (
|
|
1535
|
+
benchmark_references is not None
|
|
1536
|
+
and benchmark_references.status == "degraded"
|
|
1537
|
+
):
|
|
1538
|
+
status = "degraded"
|
|
1539
|
+
else:
|
|
1540
|
+
status = "healthy"
|
|
1541
|
+
return LocalizationHealthReport(now, status, components, providers, versions)
|