Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions schemas/desk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,43 @@ incomplete reservations.
BE-2 tests. They use synthetic data, a fixed clock and a real SQLite-backed
Miniflare DO, with manually driven alarms. This verifies local persistence,
concurrency, failures, refresh and expiry; no deployed Worker or broker is used.

## Delivery read mapping

GET `/api/desk/alerts/delivery` uses the existing console session and the same
`notificationLog` adapter as `/api/notifications/send-log`. It performs no send,
collector, dispatch or business-state write. The v1 extension `source` is optional
for existing fixtures and consumers; new responses always include it. Existing
`isAlertDelivery` accepts the unchanged unknown channel and null summaries and
ignores additive fields. F1 must validate `source` before rendering its details;
its endpoint remains explicitly mock until that separate frontend change.

The mapping below is frozen before implementing the adapter.

| Existing log field | Desk projection | Meaning and absent-data rule |
|---|---|---|
| `read_status` | `source.read_status` | Preserve complete/partial/no_data/unknown/read_failed/not_connected; invalid becomes unknown. Complete is bounded read coverage, never delivery. |
| `read_reason_codes` | `source.read_reason_codes` | Fixed allowlist only, including coverage gaps, missing/expired receipts and sender skips. |
| `as_of` | `source.as_of` | Read time, nullable; never send/composition/ACK time. |
| `items[].source_run_id`, `source_run_attempt` | `source.items[]` same fields | Positive safe integers or null. Each row stays independent. |
| `items[].workflow_updated_at` | Same field | Nullable workflow metadata, never send time or freshness proof. |
| `items[].business_day`, `report_date` | Same fields | Nullable date labels; no inferred window boundaries or correlation. |
| `items[].status` | `source.items[].sender_status` | Fixed sender/receipt enum; includes receipt_missing, receipt_expired, sender_skipped, read_failed and unknown. Expiry is stale artifact evidence, not fresh channel health. |
| `items[].delivered`, `dry_run`, `test_message` | `sender_accepted`, `dry_run`, `test_message` | Preserve booleans or null; sender acceptance never proves receiver ACK. Partial reads keep individual results. |
| `items[].read_error_code` | Same field | Existing shared fixed error-code allowlist, otherwise null. Exceptions never appear. |
| No receiver receipt | `source.items[].receiver_ack = "unknown"` | Always unknown for this source, even for delivered or partially accepted senders. |
| No channel-health observation | `channel.status` unknown | read_failed for a failed read, source_unavailable for disconnected/no-data, otherwise not_supported. Never infer current health from historical sends. |
| No verified send time/outcome/recipient count | `last_delivery = null` | All required facts must be supplied by a future reviewed source before populating this object. |
| No schedule/composition time/target or run counts | `digest = null` | Do not use item count, workflow time or date labels as substitutes. |
| `status`, `unavailable_reason`, `coverage`, `next_cursor`, `run_limit`, `truncated`, `read_failure_count`, item `id`, `source_sha`, `source_url`, `channel`, `receipt_status`, `reason_code`, `sent_at` | Not copied | Coverage comes from read_status/reasons; source locators and free text are omitted; the current public receipt has no sent_at. |

`generated_at` is projection time and `channel.name` is the adapter's fixed
`telegram` label. Unknown source shapes are never empty-success: malformed
collections become unknown with an invalid_notification_log reason. Reader
exceptions produce read_failed without provider details. There is no merge or
join across rows, runs, attempts, report dates or business days. There is no
new TTL or relaxed receipt window: receipt_expired is retained verbatim;
workflow age alone cannot establish staleness or current channel health.
A fresh HTTP 200 and ok envelope mean the read projection succeeded, not that
a notification arrived. Source integration and offline tests do not establish
active serving routes, deployed adoption or runtime delivery.
277 changes: 277 additions & 0 deletions schemas/desk/alert-delivery.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,283 @@
},
{"type": "null"}
]
},
"source": {
"type": "object",
"description": "Optional v1 sender-log evidence. Read coverage and sender acceptance never establish receiver acknowledgement.",
"properties": {
"read_status": {
"type": "string",
"enum": [
"complete",
"partial",
"no_data",
"unknown",
"read_failed",
"not_connected"
]
},
"read_reason_codes": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"enum": [
"run_listing_coverage_unknown",
"run_listing_truncated",
"run_listing_incomplete",
"notification_receipts_read_failed",
"receipt_missing",
"receipt_expired",
"sender_skipped",
"no_completed_workflow_runs",
"notification_log_not_connected",
"invalid_notification_log"
]
}
},
"as_of": {
"anyOf": [
{
"$ref": "https://schemas.example.invalid/qsl/desk/common.json#/$defs/Time"
},
{
"type": "null"
}
]
},
"items": {
"type": "array",
"maxItems": 5,
"items": {
"type": "object",
"properties": {
"source_run_id": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"source_run_attempt": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"workflow_updated_at": {
"anyOf": [
{
"$ref": "https://schemas.example.invalid/qsl/desk/common.json#/$defs/Time"
},
{
"type": "null"
}
]
},
"business_day": {
"anyOf": [
{
"$ref": "https://schemas.example.invalid/qsl/desk/common.json#/$defs/Date"
},
{
"type": "null"
}
]
},
"report_date": {
"anyOf": [
{
"$ref": "https://schemas.example.invalid/qsl/desk/common.json#/$defs/Date"
},
{
"type": "null"
}
]
},
"sender_status": {
"type": "string",
"enum": [
"route_check",
"dry_run",
"blocked_missing_secrets",
"delivered",
"send_failed",
"skipped_duplicate",
"blocked_resend_guard",
"business_digest_failed",
"skipped_empty",
"receipt_missing",
"receipt_expired",
"sender_skipped",
"read_failed",
"unknown"
]
},
"sender_accepted": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"dry_run": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"test_message": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"receiver_ack": {
"const": "unknown"
},
"read_error_code": {
"anyOf": [
{
"type": "string",
"enum": [
"receipt_read_timeout",
"receipt_too_large",
"receipt_body_missing",
"invalid_receipt_archive",
"public_receipt_missing",
"invalid_public_receipt",
"run_listing_failed",
"run_listing_transport_failed",
"run_listing_http_other",
"run_listing_http_401",
"run_listing_http_403",
"run_listing_http_404",
"run_listing_http_429",
"run_listing_http_500",
"run_listing_http_502",
"run_listing_http_503",
"run_listing_http_504",
"artifact_listing_failed",
"artifact_listing_transport_failed",
"artifact_listing_http_other",
"artifact_listing_http_401",
"artifact_listing_http_403",
"artifact_listing_http_404",
"artifact_listing_http_429",
"artifact_listing_http_500",
"artifact_listing_http_502",
"artifact_listing_http_503",
"artifact_listing_http_504",
"artifact_archive_failed",
"artifact_archive_transport_failed",
"artifact_archive_http_other",
"artifact_archive_http_401",
"artifact_archive_http_403",
"artifact_archive_http_404",
"artifact_archive_http_429",
"artifact_archive_http_500",
"artifact_archive_http_502",
"artifact_archive_http_503",
"artifact_archive_http_504",
"artifact_download_failed",
"artifact_download_transport_failed",
"artifact_download_http_other",
"artifact_download_http_401",
"artifact_download_http_403",
"artifact_download_http_404",
"artifact_download_http_429",
"artifact_download_http_500",
"artifact_download_http_502",
"artifact_download_http_503",
"artifact_download_http_504",
"receipt_decode_failed",
"receipt_decode_transport_failed",
"receipt_decode_http_other",
"receipt_decode_http_401",
"receipt_decode_http_403",
"receipt_decode_http_404",
"receipt_decode_http_429",
"receipt_decode_http_500",
"receipt_decode_http_502",
"receipt_decode_http_503",
"receipt_decode_http_504",
"receipt_projection_failed",
"receipt_projection_transport_failed",
"receipt_projection_http_other",
"receipt_projection_http_401",
"receipt_projection_http_403",
"receipt_projection_http_404",
"receipt_projection_http_429",
"receipt_projection_http_500",
"receipt_projection_http_502",
"receipt_projection_http_503",
"receipt_projection_http_504",
"sender_jobs_failed",
"sender_jobs_transport_failed",
"sender_jobs_http_other",
"sender_jobs_http_401",
"sender_jobs_http_403",
"sender_jobs_http_404",
"sender_jobs_http_429",
"sender_jobs_http_500",
"sender_jobs_http_502",
"sender_jobs_http_503",
"sender_jobs_http_504"
]
},
{
"type": "null"
}
]
}
},
"required": [
"source_run_id",
"source_run_attempt",
"workflow_updated_at",
"business_day",
"report_date",
"sender_status",
"sender_accepted",
"dry_run",
"test_message",
"receiver_ack",
"read_error_code"
],
"additionalProperties": false
}
}
},
"required": [
"read_status",
"read_reason_codes",
"as_of",
"items"
],
"additionalProperties": false
}
},
"required": ["ok", "schema_version", "generated_at", "channel", "last_delivery", "digest"],
Expand Down
8 changes: 8 additions & 0 deletions tests/console_api_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ try {
assert.equal((await call(path, { cookie: null })).status, 401);
assert.equal((await call(path, { method: "POST", body: {} })).status, 405);
}
const deskDelivery = await call("/api/desk/alerts/delivery", { cookie: reader });
assert.equal(deskDelivery.status, 200);
assert.equal(deskDelivery.body.schema_version, "qsl.desk.alert_delivery.v1");
assert.equal(deskDelivery.body.source.read_status, "not_connected");
assert.equal(deskDelivery.body.channel.status.status, "unknown");
assert.equal(deskDelivery.body.last_delivery, null);
assert.equal((await call("/api/desk/alerts/delivery", { cookie: null })).status, 401);
assert.equal((await call("/api/desk/alerts/delivery", { method: "POST", body: {} })).status, 405);
const controls = await call("/api/accounts/run-controls?account=synthetic-alpha");
validate("run_controls", controls.body);
assert.equal(controls.body.actions.enable.enabled, false);
Expand Down
3 changes: 3 additions & 0 deletions tests/desk_contract_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,6 @@ for (const scenario of ["ok", "degraded"]) {
}
}
console.log(`Desk contract validation passed: ${expected.length} responses, ${expectedFiles.length} fixtures, ${objectChecks} closed objects and negative contract checks`);

// Exercise the real delivery route and validate its additive v1 evidence.
await import("./desk_delivery_read_validation.mjs");
Loading
Loading