primcli 3.0.0-preview.7 → 3.0.0-preview.9

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.
@@ -1,4 +1,4 @@
1
- import { B as ensureSearchReportsReplyState, Ht as getEmail, J as SENT_EMAIL_EVENT_TYPES, K as replyStateSurfaceForOperation, L as ReplyStateUnsupportedError, R as assertAwaitingFilterRows, W as isAwaitingRejectedError, Wn as sdk_gen_exports, Y as WEBHOOK_EVENT_TYPES, Zt as getOwnGrant, at as automatedRejectedError, ct as expectedAutomatedVerdicts, gn as listEndpoints, it as assertAutomatedVerdict, ot as automatedSurfaceForOperation, q as EMAIL_RULE_REQUEST_EVENT_TYPES, rt as AutomatedFilterUnsupportedError, st as ensureSearchReportsAutomated, tn as getThread, ut as isAutomatedRejectedError, v as parseRetryAfter, z as awaitingRejectedError } from "./repeats-Dj_o6c-X-C6tOKAqk.js";
1
+ import { B as ensureSearchReportsReplyState, Ht as getEmail, J as SENT_EMAIL_EVENT_TYPES, K as replyStateSurfaceForOperation, L as ReplyStateUnsupportedError, R as assertAwaitingFilterRows, W as isAwaitingRejectedError, Wn as sdk_gen_exports, Y as WEBHOOK_EVENT_TYPES, Zt as getOwnGrant, at as automatedRejectedError, ct as expectedAutomatedVerdicts, gn as listEndpoints, it as assertAutomatedVerdict, ot as automatedSurfaceForOperation, q as EMAIL_RULE_REQUEST_EVENT_TYPES, rt as AutomatedFilterUnsupportedError, st as ensureSearchReportsAutomated, tn as getThread, ut as isAutomatedRejectedError, v as parseRetryAfter, z as awaitingRejectedError } from "./repeats-Dj_o6c-X-DqcHvqvW.js";
2
2
  import { C as saveCliCredentials, F as agentProfilesDirectory, H as requireDefaultLoginProfile, N as agentProfileDirectory, R as loadConnectedAgentProfile, S as resolveCliAuth, W as PrimitiveApiClient, _ as deleteCliCredentials, b as loadCliCredentials, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, i as loadCliConfig, j as AGENT_PROFILE_ENV, m as cliAccessTokenExpiresAt, p as acquireCliCredentialsLock, w as saveSignupCredentials, x as normalizeApiBaseUrl, y as detectPrimitiveKeyEnvMisname } from "./cli-config-Ms_0vNjq.js";
3
3
  import "./oclif/global-org-flag.js";
4
4
  import { a as thrownErrorBody } from "./json-output-ch756TXV.js";
@@ -602,7 +602,7 @@ const operationManifest = [
602
602
  "binaryResponse": false,
603
603
  "bodyRequired": true,
604
604
  "command": "update-account",
605
- "description": "Update the mutable account settings (such as display name and notification preferences) for the authenticated organization.",
605
+ "description": "Update the organization's spam threshold (spam_threshold) and whether an email's content is discarded once a webhook confirms receipt (discard_content_on_webhook_confirmed), the opt-in POST /emails/{id}/discard-content needs.",
606
606
  "hasJsonBody": true,
607
607
  "method": "PATCH",
608
608
  "operationId": "updateAccount",
@@ -7128,12 +7128,12 @@ const operationManifest = [
7128
7128
  },
7129
7129
  "hold": {
7130
7130
  "type": "object",
7131
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
7131
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
7132
7132
  "required": ["reason", "since"],
7133
7133
  "properties": {
7134
7134
  "reason": {
7135
7135
  "type": "string",
7136
- "description": "Open: credential_reuse_hold."
7136
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
7137
7137
  },
7138
7138
  "since": {
7139
7139
  "type": ["string", "null"],
@@ -10557,7 +10557,7 @@ const operationManifest = [
10557
10557
  "binaryResponse": false,
10558
10558
  "bodyRequired": true,
10559
10559
  "command": "revoke-credential",
10560
- "description": "Revoke one credential by kind and id. A Function key is rotated, never revoked (409 rotate_instead). An API key is revoked only by a person whose own access covers the key's access (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. What the credential set up or owns is listed in standing_objects for review, and for an API key also what it carries on for the keys it was rotated from. An organization-owned object (org_owned true) keeps working; any other keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to, except that a block filter an app made keeps blocking, within the reach the app had, unless the person revoking chose to stop it. An API key (kind org_key or connected_key) that is already revoked, and whose record is kept, answers 200 with already_revoked true and revoked 0, to a caller who could have revoked it; anyone else gets the answer they would get for a key that does not exist. The person's own Primitive CLI login revokes an API key (kind org_key) as the person, with no step-up unless the organization's credential policy sets cli_writes_require_approval (then it takes a keys.create step-up); every other kind (a connected key named as org_key included, 403 grant_forbidden target_not_covered), and every other app even with a step-up, is refused outside the dashboard (403). Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:manage. API keys, agent connection keys and agents are refused.",
10560
+ "description": "Revoke one credential by kind and id. A Function key is rotated, never revoked (409 rotate_instead). An API key is revoked by the person who holds it, whatever it reaches, or by a person whose own access covers the key's access (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. Revoking an API key also revokes the keys it replaced in its rotation that are still in their overlap and that you may revoke (predecessors_revoked_count; predecessors_left counts the rest), so its webhooks stop at once; the keys that replaced it carry its work on. Revoking a key already revoked does the same for those keys. What the credential set up or owns is listed in standing_objects for review, and for an API key also what it carries on for the keys it was rotated from, each saying whether anything still acts for it (still_acting, with the key in acting_key_id) and, for a webhook, whether it is still delivering. An organization-owned object (org_owned true) keeps working; any other keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to, except that a block filter an app made keeps blocking, within the reach the app had, unless the person revoking chose to stop it. An API key (kind org_key or connected_key) that is already revoked, and whose record is kept, answers 200 with already_revoked true, to a caller who could have revoked it, and revoked counts only the keys it replaced that were revoked now; anyone else gets the answer they would get for a key that does not exist. The person's own Primitive CLI login revokes an API key (kind org_key) as the person, with no step-up unless the organization's credential policy sets cli_writes_require_approval (then it takes a keys.create step-up); every other kind (a connected key named as org_key included, 403 grant_forbidden target_not_covered), and every other app even with a step-up, is refused outside the dashboard (403). Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:manage. API keys, agent connection keys and agents are refused.",
10561
10561
  "hasJsonBody": true,
10562
10562
  "method": "POST",
10563
10563
  "operationId": "revokeCredential",
@@ -10670,7 +10670,7 @@ const operationManifest = [
10670
10670
  }
10671
10671
  },
10672
10672
  "already_revoked": {
10673
- "description": "true when the API key was already revoked, so nothing changed and revoked is 0. Absent otherwise. GET /v1/keys/{id} says when and by whom, to a caller who may read the key.",
10673
+ "description": "true when the API key was already revoked. revoked then counts only the keys it replaced that were revoked now. Absent otherwise. GET /v1/keys/{id} says when and by whom, to a caller who may read the key.",
10674
10674
  "type": "boolean"
10675
10675
  },
10676
10676
  "keep_failed": {
@@ -10698,11 +10698,23 @@ const operationManifest = [
10698
10698
  "id": { "type": "string" },
10699
10699
  "label": {
10700
10700
  "type": "string",
10701
- "description": "A webhook's URL, a pull subscription's name (or pull when it has none), a route's or filter's pattern, or a wake schedule's command and the address it wakes (command to address). A Function trigger's label is function: look it up by id."
10701
+ "description": "A webhook's scheme and host, followed by \"/... (path and query hidden)\" when its URL has more (GET /v1/endpoints/{id} has the whole URL), a pull subscription's name (or pull when it has none), a route's or filter's pattern, or a wake schedule's command and the address it wakes (command to address). A Function trigger's label is function: look it up by id."
10702
10702
  },
10703
10703
  "org_owned": {
10704
10704
  "type": "boolean",
10705
10705
  "description": "true: the organization owns it, and it keeps working. false: the credential owns it itself, as with everything a third-party app or agent delegate made, a webhook made with a narrowed API key, or a wake schedule made with a key that does not cover super_admin. It keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to or kept it. Revoking a third-party app or agent delegate turns these off, except that a block filter it made keeps blocking, within the reach the app had, unless the person revoking chose to stop it."
10706
+ },
10707
+ "still_acting": {
10708
+ "description": "For an API key: whether anything still acts for it after this revoke (the organization, a key of the same rotation that still works, or whoever took it over). Null when that could not be read.",
10709
+ "type": ["boolean", "null"]
10710
+ },
10711
+ "acting_key_id": {
10712
+ "description": "For an API key: the key that still acts for it, shown to a caller who may read the organization's API keys. Null otherwise, and when no key does.",
10713
+ "type": ["string", "null"]
10714
+ },
10715
+ "delivering": {
10716
+ "description": "For an API key's webhook: whether it is still sent received mail, as GET /v1/endpoints/{id} delivery.delivering says. Null when that could not be read.",
10717
+ "type": ["boolean", "null"]
10706
10718
  }
10707
10719
  },
10708
10720
  "required": [
@@ -10713,6 +10725,28 @@ const operationManifest = [
10713
10725
  ],
10714
10726
  "additionalProperties": {}
10715
10727
  }
10728
+ },
10729
+ "predecessors_revoked_count": {
10730
+ "description": "For an API key: how many keys it replaced in its rotation, still in their overlap, were revoked with it. Keys that replaced it never are.",
10731
+ "type": "integer",
10732
+ "minimum": -9007199254740991,
10733
+ "maximum": 9007199254740991
10734
+ },
10735
+ "predecessors_left": {
10736
+ "description": "For an API key: how many such keys were left working because you may not revoke them. They work until their overlap ends.",
10737
+ "type": "integer",
10738
+ "minimum": -9007199254740991,
10739
+ "maximum": 9007199254740991
10740
+ },
10741
+ "predecessors_revoked": {
10742
+ "description": "The ids of the keys counted in predecessors_revoked_count, to a caller who may read the organization's API keys.",
10743
+ "type": "array",
10744
+ "items": { "type": "string" }
10745
+ },
10746
+ "predecessors_not_revoked": {
10747
+ "description": "The ids of the keys counted in predecessors_left, to a caller who may read the organization's API keys.",
10748
+ "type": "array",
10749
+ "items": { "type": "string" }
10716
10750
  }
10717
10751
  },
10718
10752
  "required": [
@@ -10731,7 +10765,7 @@ const operationManifest = [
10731
10765
  "binaryResponse": false,
10732
10766
  "bodyRequired": true,
10733
10767
  "command": "rotate-key",
10734
- "description": "Mint a successor with a new id and the same name, grant, expiry and conditions, and return its secret once. The old key keeps working for overlap_seconds, then stops: 24 hours when overlap_seconds is omitted, at most 7 days, never past its own expires_at, and at once with 0 (which is how to rotate a key whose secret leaked). previous_key_expires_at says when the old key stops. Rotating does not extend a key: the successor keeps the old expiry unless expires_at gives it a new one. That needs keys:manage (403 forbidden otherwise), is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and someone limited to their own keys can only keep or bring it forward (403 grant_forbidden, details.reason own_key_tighten_only); the old key then never outlives the new one. The person rotating must cover the key's access (403 grant_forbidden, details.reason target_not_covered), and a key an owner holds is rotated by an owner only (403 forbidden, details.reason owner_key_rotation), since rotating hands over the new secret. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10768
+ "description": "Mint a successor with a new id and the same name, grant, expiry and conditions, and return its secret once. The old key keeps working for overlap_seconds, then stops: 24 hours when overlap_seconds is omitted, at most 7 days, never past its own expires_at, and at once with 0 (which is how to rotate a key whose secret leaked). previous_key_expires_at says when the old key stops. Revoking the successor during the overlap also revokes the old key when you may revoke that one too; the revoke's predecessors_left counts any left working. Rotating does not extend a key: the successor keeps the old expiry unless expires_at gives it a new one. That needs keys:manage (403 forbidden otherwise), is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and someone limited to their own keys can only keep or bring it forward (403 grant_forbidden, details.reason own_key_tighten_only); the old key then never outlives the new one. The person rotating must cover the key's access (403 grant_forbidden, details.reason target_not_covered), and a key an owner holds is rotated by an owner only (403 forbidden, details.reason owner_key_rotation), since rotating hands over the new secret. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10735
10769
  "hasJsonBody": true,
10736
10770
  "method": "POST",
10737
10771
  "operationId": "rotateKey",
@@ -10905,7 +10939,7 @@ const operationManifest = [
10905
10939
  "binaryResponse": false,
10906
10940
  "bodyRequired": true,
10907
10941
  "command": "update-key",
10908
- "description": "Rename a key, change its expiry, its grant or its conditions, or suspend it. A later expires_at extends the key, which is how to keep a key working past its expiry (rotating keeps it, unless the rotation gives the new key an expires_at): it is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and like any loosening needs the person to be allowed to create a key with the key's access. An expired key's expiry cannot be changed (409 conflict, details.reason key_expired); make a new key instead. Any change needs the person's own access to cover the key's access now (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. A grant change is a new version of the key and is held to the same rules as creating one (a custom grant that does not say which mail it reaches is refused with details.reason mail_scope_required), so it takes the same step-up. It replaces the whole grant: send expected_grant_ref (grant.ref from GET /v1/keys/{id}) to have it refused (409 conflict, details.reason grant_changed) if the grant changed since that read; a grant change sent without it answers grant_unguarded: true. On a key limited to selected resources (grant.resources), a grant change must drop the limits on purpose with a custom grant and grant.resources {}; one that leaves them out is refused (409 conflict, details.reason key_has_resource_limits) and nothing changes. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10942
+ "description": "Rename a key, change its expiry, its grant or its conditions, or suspend it. A later expires_at extends the key, which is how to keep a key working past its expiry (rotating keeps it, unless the rotation gives the new key an expires_at): it is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and like any loosening needs the person to be allowed to create a key with the key's access. An expired key's expiry cannot be changed (409 conflict, details.reason key_expired); make a new key instead. Any change needs the person's own access to cover the key's access now (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. The one exception is suspending a key (suspended true with no other change; expected_grant_ref may accompany it), which the person who holds it may do whatever the key reaches, calling as themself rather than through another app; lifting a suspension is a change like any other. A grant change is a new version of the key and is held to the same rules as creating one (a custom grant that does not say which mail it reaches is refused with details.reason mail_scope_required), so it takes the same step-up. It replaces the whole grant: send expected_grant_ref (grant.ref from GET /v1/keys/{id}) to have it refused (409 conflict, details.reason grant_changed) if the grant changed since that read; a grant change sent without it answers grant_unguarded: true. On a key limited to selected resources (grant.resources), a grant change must drop the limits on purpose with a custom grant and grant.resources {}; one that leaves them out is refused (409 conflict, details.reason key_has_resource_limits) and nothing changes. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10909
10943
  "hasJsonBody": true,
10910
10944
  "method": "PATCH",
10911
10945
  "operationId": "updateKey",
@@ -12913,11 +12947,463 @@ const operationManifest = [
12913
12947
  "tag": "Domains",
12914
12948
  "tagCommand": "domains"
12915
12949
  },
12950
+ {
12951
+ "binaryResponse": false,
12952
+ "bodyRequired": false,
12953
+ "command": "get-email-rule-covering-me",
12954
+ "description": "One rule that covers one of your own mailboxes, read-only, with its current version; or one that no longer does but decided on mail at your own mailbox (covers_now false), shown as the version that last decided on your mail, without its current name. You see its name, wording, current tree, state, kind and author, and which of your own mailboxes it reaches. Its wording and tree are shown as written, so they can name other addresses (a condition on who an email is from or to). You do not see the other inboxes and domains its scope names, its saved expectations, or the keys, shares and webhooks that use it. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature. Only on your own dashboard session or the Primitive CLI.",
12955
+ "hasJsonBody": false,
12956
+ "method": "GET",
12957
+ "operationId": "getEmailRuleCoveringMe",
12958
+ "path": "/email-rules/covering-me/{id}",
12959
+ "pathParams": [{
12960
+ "description": "The rule id.",
12961
+ "enum": null,
12962
+ "name": "id",
12963
+ "required": true,
12964
+ "type": "string"
12965
+ }],
12966
+ "queryParams": [],
12967
+ "headerParams": [],
12968
+ "permissions": ["mail:read"],
12969
+ "access": null,
12970
+ "requestSchema": null,
12971
+ "responseSchema": {
12972
+ "type": "object",
12973
+ "properties": {
12974
+ "id": {
12975
+ "type": "string",
12976
+ "format": "uuid"
12977
+ },
12978
+ "name": {
12979
+ "description": "Null when covers_now is false: its name now may describe something else.",
12980
+ "type": ["string", "null"]
12981
+ },
12982
+ "state": {
12983
+ "type": "string",
12984
+ "enum": [
12985
+ "draft",
12986
+ "observe",
12987
+ "active"
12988
+ ],
12989
+ "description": "draft: it decides nothing. observe: it decides and records, and shares nothing. active: it decides and, for a grant rule, shares what it matches."
12990
+ },
12991
+ "kind": {
12992
+ "type": "string",
12993
+ "enum": ["grant", "do_not_share"],
12994
+ "description": "grant: what it matches can be shared. do_not_share: it blocks sharing what it matches."
12995
+ },
12996
+ "author": {
12997
+ "type": "object",
12998
+ "properties": {
12999
+ "user_id": {
13000
+ "type": "string",
13001
+ "format": "uuid"
13002
+ },
13003
+ "name": {
13004
+ "description": "Their name; null when they have none set.",
13005
+ "type": ["string", "null"]
13006
+ },
13007
+ "email": {
13008
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
13009
+ "type": ["string", "null"]
13010
+ }
13011
+ },
13012
+ "required": [
13013
+ "user_id",
13014
+ "name",
13015
+ "email"
13016
+ ]
13017
+ },
13018
+ "is_author": {
13019
+ "type": "boolean",
13020
+ "description": "Whether you wrote it."
13021
+ },
13022
+ "covers": {
13023
+ "description": "Null when it no longer covers your mailboxes and you see it because it decided on your mail.",
13024
+ "anyOf": [{
13025
+ "type": "object",
13026
+ "properties": {
13027
+ "addresses": {
13028
+ "type": "array",
13029
+ "items": { "type": "string" },
13030
+ "description": "Your own mailboxes (your member mailbox and your personal agents' addresses) it reaches now, lowercase."
13031
+ },
13032
+ "via": {
13033
+ "type": "string",
13034
+ "enum": [
13035
+ "author_reach",
13036
+ "selected",
13037
+ "own_mailboxes"
13038
+ ],
13039
+ "description": "author_reach: it runs over everything its author can read, which includes these. selected: it names these mailboxes or their domain. own_mailboxes: it is your own rule over your own mail."
13040
+ }
13041
+ },
13042
+ "required": ["addresses", "via"]
13043
+ }, { "type": "null" }]
13044
+ },
13045
+ "current_version": {
13046
+ "type": ["integer", "null"],
13047
+ "exclusiveMinimum": 0,
13048
+ "description": "Null when covers_now is false."
13049
+ },
13050
+ "created_at": {
13051
+ "type": "string",
13052
+ "format": "date-time"
13053
+ },
13054
+ "covers_now": {
13055
+ "type": "boolean",
13056
+ "description": "Whether it covers one of your own mailboxes now. False for a rule you see only because it decided on mail at your own mailbox before."
13057
+ },
13058
+ "current": {
13059
+ "description": "Its current version: what it asks of each email. When covers_now is false, the newest version that decided on your mail instead.",
13060
+ "anyOf": [{
13061
+ "type": "object",
13062
+ "properties": {
13063
+ "version": {
13064
+ "type": "integer",
13065
+ "exclusiveMinimum": 0
13066
+ },
13067
+ "english": {
13068
+ "type": "string",
13069
+ "description": "What its author asked for, in their words."
13070
+ },
13071
+ "tree": {
13072
+ "type": "object",
13073
+ "properties": {
13074
+ "version": {
13075
+ "type": "number",
13076
+ "const": 1
13077
+ },
13078
+ "vocabulary": {
13079
+ "type": "string",
13080
+ "description": "Vocabulary version, such as v1."
13081
+ },
13082
+ "when": {
13083
+ "type": "object",
13084
+ "propertyNames": { "type": "string" },
13085
+ "additionalProperties": {},
13086
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
13087
+ },
13088
+ "questions": {
13089
+ "type": "array",
13090
+ "items": { "type": "string" },
13091
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
13092
+ }
13093
+ },
13094
+ "required": [
13095
+ "version",
13096
+ "vocabulary",
13097
+ "when",
13098
+ "questions"
13099
+ ]
13100
+ },
13101
+ "explanation": {
13102
+ "type": "string",
13103
+ "description": "What the rule matches, as compiled."
13104
+ },
13105
+ "assumptions": {
13106
+ "type": "array",
13107
+ "items": { "type": "string" }
13108
+ },
13109
+ "compiled_by": { "type": "string" },
13110
+ "vocabulary": { "type": "string" },
13111
+ "created_at": {
13112
+ "type": "string",
13113
+ "format": "date-time"
13114
+ }
13115
+ },
13116
+ "required": [
13117
+ "version",
13118
+ "english",
13119
+ "tree",
13120
+ "explanation",
13121
+ "assumptions",
13122
+ "compiled_by",
13123
+ "vocabulary",
13124
+ "created_at"
13125
+ ]
13126
+ }, { "type": "null" }]
13127
+ },
13128
+ "note": {
13129
+ "description": "Present when covers_now is false: what is withheld, and why.",
13130
+ "type": "string"
13131
+ }
13132
+ },
13133
+ "required": [
13134
+ "id",
13135
+ "name",
13136
+ "state",
13137
+ "kind",
13138
+ "author",
13139
+ "is_author",
13140
+ "covers",
13141
+ "current_version",
13142
+ "created_at",
13143
+ "covers_now",
13144
+ "current"
13145
+ ]
13146
+ },
13147
+ "sdkName": "getEmailRuleCoveringMe",
13148
+ "summary": "Get a rule over your mail",
13149
+ "tag": "Email Rule Oversight",
13150
+ "tagCommand": "email-rule-oversight"
13151
+ },
13152
+ {
13153
+ "binaryResponse": false,
13154
+ "bodyRequired": false,
13155
+ "command": "list-email-rule-decisions-on-my-mail",
13156
+ "description": "What a rule that covers your mailboxes, or decided on your mail, decided about mail at your own mailboxes, newest first: never about anyone else's mail. Each decision is in the words of the version that made it, which can be earlier than the current one: the questions it asked about that email (question_answers) and the columns its conditions read (parts). Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature. Only on your own dashboard session or the Primitive CLI.",
13157
+ "hasJsonBody": false,
13158
+ "method": "GET",
13159
+ "operationId": "listEmailRuleDecisionsOnMyMail",
13160
+ "path": "/email-rules/covering-me/{id}/decisions",
13161
+ "pathParams": [{
13162
+ "description": "The rule id.",
13163
+ "enum": null,
13164
+ "name": "id",
13165
+ "required": true,
13166
+ "type": "string"
13167
+ }],
13168
+ "queryParams": [
13169
+ {
13170
+ "default": 50,
13171
+ "description": "Decisions per page, from 1 to 100; defaults to 50.",
13172
+ "enum": null,
13173
+ "maximum": 100,
13174
+ "minimum": 1,
13175
+ "name": "limit",
13176
+ "required": false,
13177
+ "type": "integer"
13178
+ },
13179
+ {
13180
+ "description": "meta.cursor from the preceding page. Omit for the first page.",
13181
+ "enum": null,
13182
+ "name": "cursor",
13183
+ "required": false,
13184
+ "type": "string"
13185
+ },
13186
+ {
13187
+ "description": "true for matches only, false for the rest.",
13188
+ "enum": ["true", "false"],
13189
+ "name": "matched",
13190
+ "required": false,
13191
+ "type": "string"
13192
+ }
13193
+ ],
13194
+ "headerParams": [],
13195
+ "permissions": ["mail:read"],
13196
+ "access": null,
13197
+ "requestSchema": null,
13198
+ "responseSchema": {
13199
+ "type": "array",
13200
+ "items": {
13201
+ "type": "object",
13202
+ "properties": {
13203
+ "id": {
13204
+ "type": "string",
13205
+ "format": "uuid"
13206
+ },
13207
+ "email_id": {
13208
+ "type": "string",
13209
+ "format": "uuid"
13210
+ },
13211
+ "rule_version": {
13212
+ "type": "integer",
13213
+ "exclusiveMinimum": 0
13214
+ },
13215
+ "classification_id": {
13216
+ "type": "string",
13217
+ "format": "uuid"
13218
+ },
13219
+ "matched": { "type": "boolean" },
13220
+ "near_miss": {
13221
+ "type": "boolean",
13222
+ "description": "Not matched, but nearly. False when detail_withheld is true."
13223
+ },
13224
+ "parts": {
13225
+ "type": "array",
13226
+ "items": {},
13227
+ "description": "How each top-level part of the rule decided."
13228
+ },
13229
+ "question_answers": {
13230
+ "type": "array",
13231
+ "items": {},
13232
+ "description": "The answers to the rule's own questions, when they were asked."
13233
+ },
13234
+ "detail_withheld": {
13235
+ "description": "Present, as true, when the email's content cannot be read: matched still says what the rule decided, and parts and question_answers are empty.",
13236
+ "type": "boolean",
13237
+ "const": true
13238
+ },
13239
+ "created_at": {
13240
+ "type": "string",
13241
+ "format": "date-time"
13242
+ },
13243
+ "from_address": {
13244
+ "description": "The email's sender address, as GET /v1/emails/{id} shows it.",
13245
+ "type": ["string", "null"]
13246
+ },
13247
+ "subject": { "type": ["string", "null"] },
13248
+ "received_at": {
13249
+ "type": ["string", "null"],
13250
+ "format": "date-time",
13251
+ "description": "When the email arrived. Null, with the other two, when you are not shown the email (it awaits sender authentication)."
13252
+ }
13253
+ },
13254
+ "required": [
13255
+ "id",
13256
+ "email_id",
13257
+ "rule_version",
13258
+ "classification_id",
13259
+ "matched",
13260
+ "near_miss",
13261
+ "parts",
13262
+ "question_answers",
13263
+ "created_at",
13264
+ "from_address",
13265
+ "subject",
13266
+ "received_at"
13267
+ ]
13268
+ }
13269
+ },
13270
+ "sdkName": "listEmailRuleDecisionsOnMyMail",
13271
+ "summary": "List what a rule decided about your mail",
13272
+ "tag": "Email Rule Oversight",
13273
+ "tagCommand": "email-rule-oversight"
13274
+ },
13275
+ {
13276
+ "binaryResponse": false,
13277
+ "bodyRequired": false,
13278
+ "command": "list-email-rules-covering-me",
13279
+ "description": "Every email rule that covers one of your own mailboxes now, yours included, newest first, in any state. A rule covers you when the scope it runs with now reaches one of your own mailboxes (your member mailbox, or a personal agent's address): one whose author reads all your mail (author scope, for an author who reads the organization's mail or holds an access entry for your mailbox), or one that names your mailbox or its domain (selected). Another member's own_mailboxes rule never covers you. You see its name, wording, current tree, state, kind and author, and which of your own mailboxes it reaches. Its wording and tree are shown as written, so they can name other addresses (a condition on who an email is from or to). You do not see the other inboxes and domains its scope names, its saved expectations, or the keys, shares and webhooks that use it. A page can hold fewer rules than limit, or none, and still carry a cursor (one request reads a bounded number of rules): keep reading until meta.cursor is null. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature. Only on your own dashboard session or the Primitive CLI.",
13280
+ "hasJsonBody": false,
13281
+ "method": "GET",
13282
+ "operationId": "listEmailRulesCoveringMe",
13283
+ "path": "/email-rules/covering-me",
13284
+ "pathParams": [],
13285
+ "queryParams": [{
13286
+ "default": 50,
13287
+ "description": "Rules per page, from 1 to 100; defaults to 50.",
13288
+ "enum": null,
13289
+ "maximum": 100,
13290
+ "minimum": 1,
13291
+ "name": "limit",
13292
+ "required": false,
13293
+ "type": "integer"
13294
+ }, {
13295
+ "description": "meta.cursor from the preceding page. Omit for the first page.",
13296
+ "enum": null,
13297
+ "name": "cursor",
13298
+ "required": false,
13299
+ "type": "string"
13300
+ }],
13301
+ "headerParams": [],
13302
+ "permissions": ["mail:read"],
13303
+ "access": null,
13304
+ "requestSchema": null,
13305
+ "responseSchema": {
13306
+ "type": "array",
13307
+ "items": {
13308
+ "type": "object",
13309
+ "properties": {
13310
+ "id": {
13311
+ "type": "string",
13312
+ "format": "uuid"
13313
+ },
13314
+ "name": { "type": "string" },
13315
+ "state": {
13316
+ "type": "string",
13317
+ "enum": [
13318
+ "draft",
13319
+ "observe",
13320
+ "active"
13321
+ ],
13322
+ "description": "draft: it decides nothing. observe: it decides and records, and shares nothing. active: it decides and, for a grant rule, shares what it matches."
13323
+ },
13324
+ "kind": {
13325
+ "type": "string",
13326
+ "enum": ["grant", "do_not_share"],
13327
+ "description": "grant: what it matches can be shared. do_not_share: it blocks sharing what it matches."
13328
+ },
13329
+ "author": {
13330
+ "type": "object",
13331
+ "properties": {
13332
+ "user_id": {
13333
+ "type": "string",
13334
+ "format": "uuid"
13335
+ },
13336
+ "name": {
13337
+ "description": "Their name; null when they have none set.",
13338
+ "type": ["string", "null"]
13339
+ },
13340
+ "email": {
13341
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
13342
+ "type": ["string", "null"]
13343
+ }
13344
+ },
13345
+ "required": [
13346
+ "user_id",
13347
+ "name",
13348
+ "email"
13349
+ ]
13350
+ },
13351
+ "is_author": {
13352
+ "type": "boolean",
13353
+ "description": "Whether you wrote it."
13354
+ },
13355
+ "covers": {
13356
+ "type": "object",
13357
+ "properties": {
13358
+ "addresses": {
13359
+ "type": "array",
13360
+ "items": { "type": "string" },
13361
+ "description": "Your own mailboxes (your member mailbox and your personal agents' addresses) it reaches now, lowercase."
13362
+ },
13363
+ "via": {
13364
+ "type": "string",
13365
+ "enum": [
13366
+ "author_reach",
13367
+ "selected",
13368
+ "own_mailboxes"
13369
+ ],
13370
+ "description": "author_reach: it runs over everything its author can read, which includes these. selected: it names these mailboxes or their domain. own_mailboxes: it is your own rule over your own mail."
13371
+ }
13372
+ },
13373
+ "required": ["addresses", "via"]
13374
+ },
13375
+ "current_version": {
13376
+ "type": ["integer", "null"],
13377
+ "exclusiveMinimum": 0
13378
+ },
13379
+ "created_at": {
13380
+ "type": "string",
13381
+ "format": "date-time"
13382
+ }
13383
+ },
13384
+ "required": [
13385
+ "id",
13386
+ "name",
13387
+ "state",
13388
+ "kind",
13389
+ "author",
13390
+ "is_author",
13391
+ "covers",
13392
+ "current_version",
13393
+ "created_at"
13394
+ ]
13395
+ }
13396
+ },
13397
+ "sdkName": "listEmailRulesCoveringMe",
13398
+ "summary": "List the rules over your mail",
13399
+ "tag": "Email Rule Oversight",
13400
+ "tagCommand": "email-rule-oversight"
13401
+ },
12916
13402
  {
12917
13403
  "binaryResponse": false,
12918
13404
  "bodyRequired": false,
12919
13405
  "command": "accept-email-rule-request",
12920
- "description": "Writes the rule as yours, over your own mailboxes only, turns it on, and shares what it matches from now on with the member who asked. Send no body to accept it as asked, or change it first with english and tree together (and an explanation, or the server writes one). Because this gives your mail to someone else, it is limited to the dashboard and the Primitive CLI: other OAuth apps and agent tokens are refused. Before accepting, test it on your mail: pass the request's tree to POST /v1/email-rules/test. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up. Nothing is stored, and it needs no access beyond testing a rule of your own. Accepting also needs mail:share_own (sharing your own mail through email rules, which only shows it and never sends): a role without it is refused, whether or not it can send. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
13406
+ "description": "Writes the rule as yours, over your own mailboxes only, turns it on, and shares what it matches from now on with the member who asked: each email in full with its attachments, but never raw. It does not go back through mail already received. Send no body to accept it as asked, or change it first with english and tree together (and an explanation, or the server writes one). Because this gives your mail to someone else, it is limited to the dashboard and the Primitive CLI: other OAuth apps and agent tokens are refused. Before accepting, test it on your mail: pass the request's tree to POST /v1/email-rules/test. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up, or have look-alikes written for it (POST /v1/email-rules/look-alikes with request_id; primitive email-rules generate-look-alikes). Neither stores anything, and neither needs access beyond testing a rule of your own; the accept keeps any you send as expectation_samples on the rule it writes. Made-up emails sent in expectation_samples are kept on the rule this writes, where GET /v1/email-rules/{id} lists them. Accepting also needs mail:share_own (sharing your own mail through email rules, which only shows it and never sends): a role without it is refused, whether or not it can send. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
12921
13407
  "hasJsonBody": true,
12922
13408
  "method": "POST",
12923
13409
  "operationId": "acceptEmailRuleRequest",
@@ -12972,7 +13458,7 @@ const operationManifest = [
12972
13458
  "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
12973
13459
  },
12974
13460
  "questions": {
12975
- "description": "Yes/no questions asked once the conditions match; all must be yes. Left out, none.",
13461
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes. Left out, none.",
12976
13462
  "type": "array",
12977
13463
  "items": { "type": "string" }
12978
13464
  }
@@ -13034,6 +13520,112 @@ const operationManifest = [
13034
13520
  },
13035
13521
  "required": ["checked", "matched"]
13036
13522
  }, { "type": "null" }]
13523
+ },
13524
+ "expectation_samples": {
13525
+ "description": "Made-up emails the rule must match or not, such as look-alikes it must never match, kept on the rule this accept writes.",
13526
+ "minItems": 1,
13527
+ "maxItems": 20,
13528
+ "type": "array",
13529
+ "items": {
13530
+ "type": "object",
13531
+ "properties": {
13532
+ "expected": {
13533
+ "type": "string",
13534
+ "enum": ["match", "no_match"]
13535
+ },
13536
+ "label": {
13537
+ "type": ["string", "null"],
13538
+ "minLength": 1,
13539
+ "maxLength": 200,
13540
+ "pattern": "^[^\\u0000]*$",
13541
+ "description": "What the email stands for, such as a personal medical bill."
13542
+ },
13543
+ "email": {
13544
+ "type": "object",
13545
+ "properties": {
13546
+ "from": {
13547
+ "type": "object",
13548
+ "properties": {
13549
+ "address": {
13550
+ "type": "string",
13551
+ "maxLength": 254,
13552
+ "format": "email",
13553
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
13554
+ },
13555
+ "name": {
13556
+ "type": "string",
13557
+ "maxLength": 200,
13558
+ "pattern": "^[^\\u0000]*$"
13559
+ }
13560
+ },
13561
+ "required": ["address"]
13562
+ },
13563
+ "to": {
13564
+ "minItems": 1,
13565
+ "maxItems": 10,
13566
+ "type": "array",
13567
+ "items": {
13568
+ "type": "string",
13569
+ "maxLength": 254,
13570
+ "format": "email",
13571
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
13572
+ }
13573
+ },
13574
+ "cc": {
13575
+ "maxItems": 10,
13576
+ "type": "array",
13577
+ "items": {
13578
+ "type": "string",
13579
+ "maxLength": 254,
13580
+ "format": "email",
13581
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
13582
+ }
13583
+ },
13584
+ "subject": {
13585
+ "type": "string",
13586
+ "maxLength": 998,
13587
+ "pattern": "^[^\\u0000]*$"
13588
+ },
13589
+ "text": {
13590
+ "type": "string",
13591
+ "minLength": 1,
13592
+ "maxLength": 4e3,
13593
+ "pattern": "^[^\\u0000]*$"
13594
+ },
13595
+ "attachment_types": {
13596
+ "maxItems": 10,
13597
+ "type": "array",
13598
+ "items": {
13599
+ "type": "string",
13600
+ "minLength": 1,
13601
+ "maxLength": 255,
13602
+ "pattern": "^[^\\u0000]*$"
13603
+ }
13604
+ },
13605
+ "automated": { "type": "boolean" },
13606
+ "from_mailing_list": { "type": "boolean" },
13607
+ "spam_score": {
13608
+ "type": "number",
13609
+ "minimum": -1e3,
13610
+ "maximum": 1e3
13611
+ }
13612
+ },
13613
+ "required": [
13614
+ "from",
13615
+ "to",
13616
+ "text"
13617
+ ],
13618
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
13619
+ },
13620
+ "id": {
13621
+ "type": "string",
13622
+ "format": "uuid"
13623
+ },
13624
+ "created_at": { "type": "string" }
13625
+ },
13626
+ "required": ["expected", "email"],
13627
+ "additionalProperties": false
13628
+ }
13037
13629
  }
13038
13630
  },
13039
13631
  "additionalProperties": false
@@ -13079,7 +13671,7 @@ const operationManifest = [
13079
13671
  "questions": {
13080
13672
  "type": "array",
13081
13673
  "items": { "type": "string" },
13082
- "description": "Yes/no questions asked once the conditions match; all must be yes."
13674
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
13083
13675
  }
13084
13676
  },
13085
13677
  "required": [
@@ -13188,6 +13780,14 @@ const operationManifest = [
13188
13780
  "decline_reason": {
13189
13781
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
13190
13782
  "type": ["string", "null"]
13783
+ },
13784
+ "edited_on_accept": {
13785
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
13786
+ "type": ["boolean", "null"]
13787
+ },
13788
+ "notice": {
13789
+ "description": "For the member asked, while pending: what accepting would share. For the member who asked, once accepted with changes: that the request is not what is shared. Else null.",
13790
+ "type": ["string", "null"]
13191
13791
  }
13192
13792
  },
13193
13793
  "required": [
@@ -13234,6 +13834,10 @@ const operationManifest = [
13234
13834
  "type": "string",
13235
13835
  "enum": ["grant", "do_not_share"]
13236
13836
  },
13837
+ "protection_note": {
13838
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), a classifier fact narrows it, or its several questions are alternatives.",
13839
+ "type": "string"
13840
+ },
13237
13841
  "evaluation_scope": {
13238
13842
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
13239
13843
  "type": "string",
@@ -13337,7 +13941,7 @@ const operationManifest = [
13337
13941
  "questions": {
13338
13942
  "type": "array",
13339
13943
  "items": { "type": "string" },
13340
- "description": "Yes/no questions asked once the conditions match; all must be yes."
13944
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
13341
13945
  }
13342
13946
  },
13343
13947
  "required": [
@@ -13564,6 +14168,10 @@ const operationManifest = [
13564
14168
  "rule",
13565
14169
  "rule_id"
13566
14170
  ]
14171
+ },
14172
+ "notice": {
14173
+ "description": "What is now shared, in words.",
14174
+ "type": "string"
13567
14175
  }
13568
14176
  },
13569
14177
  "required": [
@@ -13663,7 +14271,7 @@ const operationManifest = [
13663
14271
  "binaryResponse": false,
13664
14272
  "bodyRequired": true,
13665
14273
  "command": "compile-email-rule",
13666
- "description": "Turns a plain-English description of which received emails to match into a rule tree over the published column contract, with a plain-English explanation of what it matches, the assumptions made, and warnings for anything the columns cannot express. Where the columns fall short the rule may carry up to three yes/no questions, asked about an email only after its conditions match. Nothing is saved. A rule with no questions has its conditions counted, read only, over the caller's recent classified mail (what a test of it covers by default): breadth is that count, and a warning says when it matches nearly all of it. The SQL the rule renders to is returned only when include_sql is true. kind do_not_share compiles a do-not-share rule for recall: conditions only as narrow as the description says, the judgement of what it describes put into questions, and with nothing a column decides for certain, conditions that hold for every email. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14274
+ "description": "Turns a plain-English description of which received emails to match into a rule tree over the published column contract, with a plain-English explanation of what it matches, the assumptions made, and warnings for anything the columns cannot express. Where the columns fall short the rule may carry up to five yes/no questions, asked about an email only after its conditions match; they read its subject, its new text and its attachments' file names, never what is inside an attachment, and warnings say when a question mentions attachments. Nothing is saved. A rule with no questions has its conditions counted, read only, over the caller's recent classified mail (what a test of it covers by default): breadth is that count, and a warning says when it matches nearly all of it. The SQL the rule renders to is returned only when include_sql is true. kind do_not_share compiles a do-not-share rule for recall: conditions only as narrow as the description says, the judgement of what it describes put into questions, and with nothing a column decides for certain, conditions that hold for every email. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
13667
14275
  "hasJsonBody": true,
13668
14276
  "method": "POST",
13669
14277
  "operationId": "compileEmailRule",
@@ -13724,7 +14332,7 @@ const operationManifest = [
13724
14332
  "questions": {
13725
14333
  "type": "array",
13726
14334
  "items": { "type": "string" },
13727
- "description": "Yes/no questions asked once the conditions match; all must be yes."
14335
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
13728
14336
  }
13729
14337
  },
13730
14338
  "required": [
@@ -13746,7 +14354,7 @@ const operationManifest = [
13746
14354
  "warnings": {
13747
14355
  "type": "array",
13748
14356
  "items": { "type": "string" },
13749
- "description": "Parts of the description the column contract cannot express, and how the rule handles them. When the rule reads automated or from_mailing_list, it also has a note on what that column reads and what it misses. When the rule has no questions and its conditions hold for all of the caller's recent classified mail (what a test of it covers by default), or for at least 90% of at least 20 emails, the first warning says so. That count is not made for a caller whose access shows headers only, or who could not run a test of the rule, and may also be skipped when the compile ran long or the count fails."
14357
+ "description": "Parts of the description the column contract cannot express, and how the rule handles them. It also has notes on reading automated, from_mailing_list or sender_relationship, a sender address the description does not give, and a grant worded as a prohibition. When the rule has no questions and its conditions hold for all of the caller's recent classified mail (what a test of it covers by default), or for at least 90% of at least 20 emails, the first warning says so. That count is not made for a caller whose access shows headers only, or who could not run a test of the rule, and may also be skipped when the compile ran long or the count fails."
13750
14358
  },
13751
14359
  "compiled_by": {
13752
14360
  "type": "string",
@@ -13805,7 +14413,7 @@ const operationManifest = [
13805
14413
  "binaryResponse": false,
13806
14414
  "bodyRequired": true,
13807
14415
  "command": "create-email-rule",
13808
- "description": "Saves a rule as version 1 from a compile response or a tree you wrote, checked against the column contract; without an explanation the server writes one. It starts in observe (what it matches is recorded, nothing granted or blocked) unless state is draft (not evaluated). You are its author, and it never reaches mail you cannot read. evaluation_scope is own_mailboxes (your own mail) unless you name author (all you can read) or selected; scope_selection may name only what you can read. The answer carries the scope as stored. expectations saves emails it must match and must not; the answer judges them (expectations_check). kind do_not_share blocks sharing: it runs before every other rule, and while active no rule shares an email it matches or cannot decide; a key cannot list it (details.reason rule_blocks_sharing). Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
14416
+ "description": "Saves a rule as version 1 from a compile response or a tree you wrote, checked against the column contract; without an explanation the server writes one. It starts in observe (what it matches is recorded, nothing granted or blocked) unless state is draft (not evaluated), or active for a do_not_share rule. You are its author, and it never reaches mail you cannot read. evaluation_scope is own_mailboxes (your own mail) unless you name author (all you can read) or selected; scope_selection may name only what you can read. The answer carries the scope as stored. expectations saves emails it must match and must not; the answer judges them (expectations_check). kind do_not_share blocks sharing: it runs before every other rule, and while active no rule shares an email it matches or cannot decide; a key cannot list it (details.reason rule_blocks_sharing). Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
13809
14417
  "hasJsonBody": true,
13810
14418
  "method": "POST",
13811
14419
  "operationId": "createEmailRule",
@@ -13836,9 +14444,13 @@ const operationManifest = [
13836
14444
  "enum": ["grant", "do_not_share"]
13837
14445
  },
13838
14446
  "state": {
13839
- "description": "observe (the default) records what the rule would match and grants or blocks nothing; draft is not evaluated.",
14447
+ "description": "observe (the default) records what the rule would match and grants or blocks nothing; draft is not evaluated; active only for a do_not_share rule.",
13840
14448
  "type": "string",
13841
- "enum": ["draft", "observe"]
14449
+ "enum": [
14450
+ "draft",
14451
+ "observe",
14452
+ "active"
14453
+ ]
13842
14454
  },
13843
14455
  "evaluation_scope": {
13844
14456
  "type": "string",
@@ -13899,6 +14511,112 @@ const operationManifest = [
13899
14511
  "additionalProperties": false,
13900
14512
  "description": "Emails it must match and must not (at most 100): a test by rule_id judges them unless expect is given, and each version it saves is checked against them."
13901
14513
  },
14514
+ "expectation_samples": {
14515
+ "description": "Made-up emails it must match or not (at most 20).",
14516
+ "minItems": 1,
14517
+ "maxItems": 20,
14518
+ "type": "array",
14519
+ "items": {
14520
+ "type": "object",
14521
+ "properties": {
14522
+ "expected": {
14523
+ "type": "string",
14524
+ "enum": ["match", "no_match"]
14525
+ },
14526
+ "label": {
14527
+ "type": ["string", "null"],
14528
+ "minLength": 1,
14529
+ "maxLength": 200,
14530
+ "pattern": "^[^\\u0000]*$",
14531
+ "description": "What the email stands for, such as a personal medical bill."
14532
+ },
14533
+ "email": {
14534
+ "type": "object",
14535
+ "properties": {
14536
+ "from": {
14537
+ "type": "object",
14538
+ "properties": {
14539
+ "address": {
14540
+ "type": "string",
14541
+ "maxLength": 254,
14542
+ "format": "email",
14543
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
14544
+ },
14545
+ "name": {
14546
+ "type": "string",
14547
+ "maxLength": 200,
14548
+ "pattern": "^[^\\u0000]*$"
14549
+ }
14550
+ },
14551
+ "required": ["address"]
14552
+ },
14553
+ "to": {
14554
+ "minItems": 1,
14555
+ "maxItems": 10,
14556
+ "type": "array",
14557
+ "items": {
14558
+ "type": "string",
14559
+ "maxLength": 254,
14560
+ "format": "email",
14561
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
14562
+ }
14563
+ },
14564
+ "cc": {
14565
+ "maxItems": 10,
14566
+ "type": "array",
14567
+ "items": {
14568
+ "type": "string",
14569
+ "maxLength": 254,
14570
+ "format": "email",
14571
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
14572
+ }
14573
+ },
14574
+ "subject": {
14575
+ "type": "string",
14576
+ "maxLength": 998,
14577
+ "pattern": "^[^\\u0000]*$"
14578
+ },
14579
+ "text": {
14580
+ "type": "string",
14581
+ "minLength": 1,
14582
+ "maxLength": 4e3,
14583
+ "pattern": "^[^\\u0000]*$"
14584
+ },
14585
+ "attachment_types": {
14586
+ "maxItems": 10,
14587
+ "type": "array",
14588
+ "items": {
14589
+ "type": "string",
14590
+ "minLength": 1,
14591
+ "maxLength": 255,
14592
+ "pattern": "^[^\\u0000]*$"
14593
+ }
14594
+ },
14595
+ "automated": { "type": "boolean" },
14596
+ "from_mailing_list": { "type": "boolean" },
14597
+ "spam_score": {
14598
+ "type": "number",
14599
+ "minimum": -1e3,
14600
+ "maximum": 1e3
14601
+ }
14602
+ },
14603
+ "required": [
14604
+ "from",
14605
+ "to",
14606
+ "text"
14607
+ ],
14608
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
14609
+ },
14610
+ "id": {
14611
+ "type": "string",
14612
+ "format": "uuid"
14613
+ },
14614
+ "created_at": { "type": "string" }
14615
+ },
14616
+ "required": ["expected", "email"],
14617
+ "additionalProperties": false
14618
+ }
14619
+ },
13902
14620
  "english": {
13903
14621
  "type": "string",
13904
14622
  "minLength": 1,
@@ -13923,7 +14641,7 @@ const operationManifest = [
13923
14641
  "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
13924
14642
  },
13925
14643
  "questions": {
13926
- "description": "Yes/no questions asked once the conditions match; all must be yes. Left out, none.",
14644
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes. Left out, none.",
13927
14645
  "type": "array",
13928
14646
  "items": { "type": "string" }
13929
14647
  }
@@ -14015,6 +14733,10 @@ const operationManifest = [
14015
14733
  "type": "string",
14016
14734
  "enum": ["grant", "do_not_share"]
14017
14735
  },
14736
+ "protection_note": {
14737
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), a classifier fact narrows it, or its several questions are alternatives.",
14738
+ "type": "string"
14739
+ },
14018
14740
  "evaluation_scope": {
14019
14741
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
14020
14742
  "type": "string",
@@ -14118,7 +14840,7 @@ const operationManifest = [
14118
14840
  "questions": {
14119
14841
  "type": "array",
14120
14842
  "items": { "type": "string" },
14121
- "description": "Yes/no questions asked once the conditions match; all must be yes."
14843
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
14122
14844
  }
14123
14845
  },
14124
14846
  "required": [
@@ -14206,8 +14928,114 @@ const operationManifest = [
14206
14928
  ]
14207
14929
  }
14208
14930
  },
14931
+ "expectation_samples": {
14932
+ "description": "Its saved made-up emails, when this write set or added some.",
14933
+ "type": "array",
14934
+ "items": {
14935
+ "type": "object",
14936
+ "properties": {
14937
+ "id": {
14938
+ "type": "string",
14939
+ "format": "uuid"
14940
+ },
14941
+ "expected": {
14942
+ "type": "string",
14943
+ "enum": ["match", "no_match"]
14944
+ },
14945
+ "label": { "type": ["string", "null"] },
14946
+ "email": {
14947
+ "type": "object",
14948
+ "properties": {
14949
+ "from": {
14950
+ "type": "object",
14951
+ "properties": {
14952
+ "address": {
14953
+ "type": "string",
14954
+ "maxLength": 254,
14955
+ "format": "email",
14956
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
14957
+ },
14958
+ "name": {
14959
+ "type": "string",
14960
+ "maxLength": 200,
14961
+ "pattern": "^[^\\u0000]*$"
14962
+ }
14963
+ },
14964
+ "required": ["address"]
14965
+ },
14966
+ "to": {
14967
+ "minItems": 1,
14968
+ "maxItems": 10,
14969
+ "type": "array",
14970
+ "items": {
14971
+ "type": "string",
14972
+ "maxLength": 254,
14973
+ "format": "email",
14974
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
14975
+ }
14976
+ },
14977
+ "cc": {
14978
+ "maxItems": 10,
14979
+ "type": "array",
14980
+ "items": {
14981
+ "type": "string",
14982
+ "maxLength": 254,
14983
+ "format": "email",
14984
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
14985
+ }
14986
+ },
14987
+ "subject": {
14988
+ "type": "string",
14989
+ "maxLength": 998,
14990
+ "pattern": "^[^\\u0000]*$"
14991
+ },
14992
+ "text": {
14993
+ "type": "string",
14994
+ "minLength": 1,
14995
+ "maxLength": 4e3,
14996
+ "pattern": "^[^\\u0000]*$"
14997
+ },
14998
+ "attachment_types": {
14999
+ "maxItems": 10,
15000
+ "type": "array",
15001
+ "items": {
15002
+ "type": "string",
15003
+ "minLength": 1,
15004
+ "maxLength": 255,
15005
+ "pattern": "^[^\\u0000]*$"
15006
+ }
15007
+ },
15008
+ "automated": { "type": "boolean" },
15009
+ "from_mailing_list": { "type": "boolean" },
15010
+ "spam_score": {
15011
+ "type": "number",
15012
+ "minimum": -1e3,
15013
+ "maximum": 1e3
15014
+ }
15015
+ },
15016
+ "required": [
15017
+ "from",
15018
+ "to",
15019
+ "text"
15020
+ ],
15021
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
15022
+ },
15023
+ "created_at": {
15024
+ "type": "string",
15025
+ "format": "date-time"
15026
+ }
15027
+ },
15028
+ "required": [
15029
+ "id",
15030
+ "expected",
15031
+ "label",
15032
+ "email",
15033
+ "created_at"
15034
+ ]
15035
+ }
15036
+ },
14209
15037
  "expectations_check": {
14210
- "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
15038
+ "description": "After a write that saved a version or changed its expectations or made-up emails, if it has some: the emails by id judged on that version from stored classifications without asking its questions, and any made-up emails listed as undecided. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed), and after a write that changed only made-up emails on a rule with emails by id. Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
14211
15039
  "type": "object",
14212
15040
  "properties": {
14213
15041
  "version": {
@@ -14219,7 +15047,7 @@ const operationManifest = [
14219
15047
  "type": ["boolean", "null"]
14220
15048
  },
14221
15049
  "decided": {
14222
- "description": "Whether every expectation was judged (undecided is empty). Absent from a server that predates it.",
15050
+ "description": "Whether every expectation was judged (undecided and undecided_samples are empty). Absent from a server that predates it.",
14223
15051
  "type": "boolean"
14224
15052
  },
14225
15053
  "failed": {
@@ -14236,8 +15064,24 @@ const operationManifest = [
14236
15064
  "format": "uuid"
14237
15065
  }
14238
15066
  },
15067
+ "failed_samples": {
15068
+ "description": "Made-up emails that failed, by index in the set judged (saved oldest first, then added; or as sent).",
15069
+ "type": "array",
15070
+ "items": {
15071
+ "type": "integer",
15072
+ "minimum": 0
15073
+ }
15074
+ },
15075
+ "undecided_samples": {
15076
+ "description": "Made-up emails not decided, by index. Only a write sent with if_expectations_pass that saves a version or activates the rule judges them.",
15077
+ "type": "array",
15078
+ "items": {
15079
+ "type": "integer",
15080
+ "minimum": 0
15081
+ }
15082
+ },
14239
15083
  "note": {
14240
- "description": "On an update that saved a version that did not pass: that it was saved anyway, is current, and whether it is live.",
15084
+ "description": "On an update that saved a version that did not pass, or a do_not_share rule created active: that it was saved anyway, is current, and whether it is live.",
14241
15085
  "type": "string"
14242
15086
  }
14243
15087
  },
@@ -14372,7 +15216,7 @@ const operationManifest = [
14372
15216
  "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
14373
15217
  },
14374
15218
  "questions": {
14375
- "description": "Yes/no questions asked once the conditions match; all must be yes. Left out, none.",
15219
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes. Left out, none.",
14376
15220
  "type": "array",
14377
15221
  "items": { "type": "string" }
14378
15222
  }
@@ -14481,7 +15325,7 @@ const operationManifest = [
14481
15325
  "questions": {
14482
15326
  "type": "array",
14483
15327
  "items": { "type": "string" },
14484
- "description": "Yes/no questions asked once the conditions match; all must be yes."
15328
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
14485
15329
  }
14486
15330
  },
14487
15331
  "required": [
@@ -14591,6 +15435,14 @@ const operationManifest = [
14591
15435
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14592
15436
  "type": ["string", "null"]
14593
15437
  },
15438
+ "edited_on_accept": {
15439
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
15440
+ "type": ["boolean", "null"]
15441
+ },
15442
+ "notice": {
15443
+ "description": "For the member asked, while pending: what accepting would share. For the member who asked, once accepted with changes: that the request is not what is shared. Else null.",
15444
+ "type": ["string", "null"]
15445
+ },
14594
15446
  "target_can_accept": {
14595
15447
  "description": "Whether the member asked can accept it now under their role. Null unless you can list members and their roles (members:read), or when it could not be read.",
14596
15448
  "type": ["boolean", "null"]
@@ -14702,7 +15554,7 @@ const operationManifest = [
14702
15554
  "questions": {
14703
15555
  "type": "array",
14704
15556
  "items": { "type": "string" },
14705
- "description": "Yes/no questions asked once the conditions match; all must be yes."
15557
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
14706
15558
  }
14707
15559
  },
14708
15560
  "required": [
@@ -14811,6 +15663,14 @@ const operationManifest = [
14811
15663
  "decline_reason": {
14812
15664
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14813
15665
  "type": ["string", "null"]
15666
+ },
15667
+ "edited_on_accept": {
15668
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
15669
+ "type": ["boolean", "null"]
15670
+ },
15671
+ "notice": {
15672
+ "description": "For the member asked, while pending: what accepting would share. For the member who asked, once accepted with changes: that the request is not what is shared. Else null.",
15673
+ "type": ["string", "null"]
14814
15674
  }
14815
15675
  },
14816
15676
  "required": [
@@ -14941,7 +15801,7 @@ const operationManifest = [
14941
15801
  "binaryResponse": false,
14942
15802
  "bodyRequired": false,
14943
15803
  "command": "end-email-rule-share",
14944
- "description": "Stops sharing. Either person on the share can do this, or an owner or admin who manages members. The share is kept, marked as ended. This needs no mail access and does not depend on the email rules feature. An owner or admin not on the share must use a dashboard session. Otherwise use your own session, or an OAuth or CLI token that acts with your full access: API keys and agent keys are refused.",
15804
+ "description": "Stops sharing; the rule itself is left unchanged, as the answer's note says. Either person on the share can do this, or an owner or admin who manages members. The share is kept, marked as ended. This needs no mail access and does not depend on the email rules feature. An owner or admin not on the share must use a dashboard session. Otherwise use your own session, or an OAuth or CLI token that acts with your full access: API keys and agent keys are refused.",
14945
15805
  "hasJsonBody": false,
14946
15806
  "method": "DELETE",
14947
15807
  "operationId": "endEmailRuleShare",
@@ -14960,14 +15820,339 @@ const operationManifest = [
14960
15820
  "requestSchema": null,
14961
15821
  "responseSchema": {
14962
15822
  "type": "object",
14963
- "properties": { "ended": { "type": "boolean" } },
14964
- "required": ["ended"]
15823
+ "properties": {
15824
+ "ended": { "type": "boolean" },
15825
+ "rule_id": { "type": "string" },
15826
+ "note": { "type": "string" }
15827
+ },
15828
+ "required": ["ended"],
15829
+ "additionalProperties": {}
14965
15830
  },
14966
15831
  "sdkName": "endEmailRuleShare",
14967
15832
  "summary": "End a rule share",
14968
15833
  "tag": "Email Rules",
14969
15834
  "tagCommand": "email-rules"
14970
15835
  },
15836
+ {
15837
+ "binaryResponse": false,
15838
+ "bodyRequired": true,
15839
+ "command": "generate-look-alikes",
15840
+ "description": "Writes made-up emails that come close to what a grant rule describes, and reports what the rule decides for each, as test-sample would. Most should not match (expected no_match): business near misses from its senders and topic (kind other: an all-clear, a reply with nothing new, a related document, another subject) and private mail (a personal medical bill, pay for named people, a personal purchase, personal banking or tax). By default a third, rounded down, are near cases it should match (expected match, kind other). failed means the rule decided one against expected. Nothing is stored and no mail is read. Keep them, with their expected, with add_expectation_samples, or for a request with the accept's expectation_samples. Limited to 20 an hour per member. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
15841
+ "hasJsonBody": true,
15842
+ "method": "POST",
15843
+ "operationId": "generateLookAlikes",
15844
+ "path": "/email-rules/look-alikes",
15845
+ "pathParams": [],
15846
+ "queryParams": [],
15847
+ "headerParams": [{
15848
+ "description": "Optional, and not used to deduplicate: this operation stores no result by the key, so a retry with the same key runs the request again. Operations that need a credential refuse a key that is empty, longer than 255 characters or not printable ASCII with 400 `validation_error` and echo a valid one in the Idempotency-Key response header; operations that need none ignore it.",
15849
+ "enum": null,
15850
+ "name": "Idempotency-Key",
15851
+ "required": false,
15852
+ "type": "string"
15853
+ }],
15854
+ "permissions": ["mail:read"],
15855
+ "access": null,
15856
+ "requestSchema": {
15857
+ "type": "object",
15858
+ "properties": {
15859
+ "tree": {
15860
+ "type": "object",
15861
+ "properties": {
15862
+ "version": {
15863
+ "type": "number",
15864
+ "const": 1
15865
+ },
15866
+ "vocabulary": {
15867
+ "type": "string",
15868
+ "description": "Vocabulary version, such as v1."
15869
+ },
15870
+ "when": {
15871
+ "type": "object",
15872
+ "propertyNames": { "type": "string" },
15873
+ "additionalProperties": {},
15874
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
15875
+ },
15876
+ "questions": {
15877
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes. Left out, none.",
15878
+ "type": "array",
15879
+ "items": { "type": "string" }
15880
+ }
15881
+ },
15882
+ "required": [
15883
+ "version",
15884
+ "vocabulary",
15885
+ "when"
15886
+ ],
15887
+ "additionalProperties": false
15888
+ },
15889
+ "english": {
15890
+ "description": "What the tree is meant to match; without it, the tree is described in words.",
15891
+ "type": "string",
15892
+ "minLength": 1,
15893
+ "maxLength": 2e3
15894
+ },
15895
+ "evaluation_scope": {
15896
+ "type": "string",
15897
+ "enum": [
15898
+ "author",
15899
+ "own_mailboxes",
15900
+ "selected"
15901
+ ],
15902
+ "description": "own_mailboxes (the default): mail to your own addresses and agents (for a rule that shares, those mail:share_own lets you share). author: all you can read, inboxes added later included. selected: what scope_selection names."
15903
+ },
15904
+ "scope_selection": {
15905
+ "type": "object",
15906
+ "properties": {
15907
+ "addresses": {
15908
+ "maxItems": 100,
15909
+ "type": "array",
15910
+ "items": {
15911
+ "type": "string",
15912
+ "minLength": 3,
15913
+ "maxLength": 320,
15914
+ "pattern": "^[^\\s@]+@[^\\s@]+$"
15915
+ }
15916
+ },
15917
+ "domains": {
15918
+ "maxItems": 50,
15919
+ "type": "array",
15920
+ "items": {
15921
+ "type": "string",
15922
+ "minLength": 1,
15923
+ "maxLength": 253,
15924
+ "pattern": "^[^\\s@]+\\.[^\\s@]+$"
15925
+ }
15926
+ }
15927
+ },
15928
+ "additionalProperties": false,
15929
+ "description": "For a selected rule: inboxes, and domains (each address at them the author reads); each one you can read."
15930
+ },
15931
+ "rule_id": {
15932
+ "description": "A saved rule you can see: its current version and words.",
15933
+ "type": "string",
15934
+ "format": "uuid"
15935
+ },
15936
+ "request_id": {
15937
+ "description": "A rule request you can see: its rule as asked.",
15938
+ "type": "string",
15939
+ "format": "uuid"
15940
+ },
15941
+ "kind": {
15942
+ "description": "For an unsaved tree: do_not_share tests it as a rule that blocks sharing, so an email it cannot decide counts as matched. A saved rule uses its own.",
15943
+ "type": "string",
15944
+ "enum": ["grant", "do_not_share"]
15945
+ },
15946
+ "count": {
15947
+ "description": "How many, 1 to 8; 6 by default.",
15948
+ "type": "integer",
15949
+ "minimum": 1,
15950
+ "maximum": 8
15951
+ },
15952
+ "kinds": {
15953
+ "description": "Which kinds, taken in turn, all expected no_match; other is a business near miss. Left out, a default mix.",
15954
+ "minItems": 1,
15955
+ "maxItems": 5,
15956
+ "type": "array",
15957
+ "items": {
15958
+ "type": "string",
15959
+ "enum": [
15960
+ "medical",
15961
+ "payroll",
15962
+ "personal_purchase",
15963
+ "bank_tax",
15964
+ "other"
15965
+ ]
15966
+ }
15967
+ },
15968
+ "to": {
15969
+ "description": "The mailbox they arrive in; by default the first address (alphabetically) a selected scope names that a made-up email can be sent to, preferring one the author still reads; else the rule author's (a request's target's) sign-in address when you may see it, else yours.",
15970
+ "type": "string",
15971
+ "maxLength": 254,
15972
+ "format": "email",
15973
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
15974
+ }
15975
+ },
15976
+ "additionalProperties": false
15977
+ },
15978
+ "responseSchema": {
15979
+ "type": "object",
15980
+ "properties": {
15981
+ "rule": {
15982
+ "type": "object",
15983
+ "properties": {
15984
+ "id": {
15985
+ "type": ["string", "null"],
15986
+ "format": "uuid"
15987
+ },
15988
+ "version": {
15989
+ "type": ["integer", "null"],
15990
+ "exclusiveMinimum": 0
15991
+ },
15992
+ "vocabulary": { "type": "string" }
15993
+ },
15994
+ "required": [
15995
+ "id",
15996
+ "version",
15997
+ "vocabulary"
15998
+ ]
15999
+ },
16000
+ "look_alikes": {
16001
+ "type": "array",
16002
+ "items": {
16003
+ "type": "object",
16004
+ "properties": {
16005
+ "kind": {
16006
+ "type": "string",
16007
+ "enum": [
16008
+ "medical",
16009
+ "payroll",
16010
+ "personal_purchase",
16011
+ "bank_tax",
16012
+ "other"
16013
+ ]
16014
+ },
16015
+ "expected": {
16016
+ "type": "string",
16017
+ "enum": ["match", "no_match"],
16018
+ "description": "match: a near case the rule should match; no_match: one it should not."
16019
+ },
16020
+ "why": {
16021
+ "type": "string",
16022
+ "description": "Why the rule should, or should not, match it."
16023
+ },
16024
+ "email": {
16025
+ "type": "object",
16026
+ "properties": {
16027
+ "from": {
16028
+ "type": "object",
16029
+ "properties": {
16030
+ "address": { "type": "string" },
16031
+ "name": { "type": "string" }
16032
+ },
16033
+ "required": ["address"]
16034
+ },
16035
+ "to": {
16036
+ "type": "array",
16037
+ "items": { "type": "string" }
16038
+ },
16039
+ "cc": {
16040
+ "type": "array",
16041
+ "items": { "type": "string" }
16042
+ },
16043
+ "subject": { "type": "string" },
16044
+ "text": { "type": "string" },
16045
+ "attachment_types": {
16046
+ "type": "array",
16047
+ "items": { "type": "string" }
16048
+ },
16049
+ "automated": { "type": "boolean" },
16050
+ "from_mailing_list": { "type": "boolean" }
16051
+ },
16052
+ "required": [
16053
+ "from",
16054
+ "to",
16055
+ "cc",
16056
+ "subject",
16057
+ "text",
16058
+ "attachment_types",
16059
+ "automated",
16060
+ "from_mailing_list"
16061
+ ],
16062
+ "description": "A made-up email, as test-sample takes it."
16063
+ },
16064
+ "outcome": {
16065
+ "type": "string",
16066
+ "enum": [
16067
+ "matched",
16068
+ "near_miss",
16069
+ "no_match",
16070
+ "not_decided"
16071
+ ]
16072
+ },
16073
+ "status": {
16074
+ "type": "string",
16075
+ "enum": [
16076
+ "passed",
16077
+ "failed",
16078
+ "undecided"
16079
+ ],
16080
+ "description": "failed: the rule decided it against expected (shared one it should not, or missed one it should match). undecided: the rule reads a spam score, which it cannot carry, whatever it decided; or not matched on something it could not decide (not classified, a fact or answer missing, or not reached in time)."
16081
+ },
16082
+ "question_answers": {
16083
+ "type": "array",
16084
+ "items": {
16085
+ "type": "object",
16086
+ "properties": {
16087
+ "question": { "type": "string" },
16088
+ "answer": { "type": ["boolean", "null"] },
16089
+ "probability": { "type": ["number", "null"] },
16090
+ "error_class": { "type": "string" }
16091
+ },
16092
+ "required": [
16093
+ "question",
16094
+ "answer",
16095
+ "probability"
16096
+ ]
16097
+ }
16098
+ }
16099
+ },
16100
+ "required": [
16101
+ "kind",
16102
+ "expected",
16103
+ "why",
16104
+ "email",
16105
+ "outcome",
16106
+ "status",
16107
+ "question_answers"
16108
+ ]
16109
+ }
16110
+ },
16111
+ "counts": {
16112
+ "type": "object",
16113
+ "properties": {
16114
+ "passed": {
16115
+ "type": "integer",
16116
+ "minimum": 0
16117
+ },
16118
+ "failed": {
16119
+ "type": "integer",
16120
+ "minimum": 0
16121
+ },
16122
+ "undecided": {
16123
+ "type": "integer",
16124
+ "minimum": 0
16125
+ },
16126
+ "dropped": {
16127
+ "type": "integer",
16128
+ "minimum": 0,
16129
+ "description": "Asked for but not returned: not written, outside the sample bounds, or of a kind and expected not asked for."
16130
+ }
16131
+ },
16132
+ "required": [
16133
+ "passed",
16134
+ "failed",
16135
+ "undecided",
16136
+ "dropped"
16137
+ ]
16138
+ },
16139
+ "note": {
16140
+ "type": "string",
16141
+ "description": "That the emails are made up and were never sent or stored."
16142
+ }
16143
+ },
16144
+ "required": [
16145
+ "rule",
16146
+ "look_alikes",
16147
+ "counts",
16148
+ "note"
16149
+ ]
16150
+ },
16151
+ "sdkName": "generateLookAlikes",
16152
+ "summary": "Write look-alike emails a rule should or should not match",
16153
+ "tag": "Email Rules",
16154
+ "tagCommand": "email-rules"
16155
+ },
14971
16156
  {
14972
16157
  "binaryResponse": false,
14973
16158
  "bodyRequired": false,
@@ -15016,6 +16201,10 @@ const operationManifest = [
15016
16201
  "type": "string",
15017
16202
  "enum": ["grant", "do_not_share"]
15018
16203
  },
16204
+ "protection_note": {
16205
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), a classifier fact narrows it, or its several questions are alternatives.",
16206
+ "type": "string"
16207
+ },
15019
16208
  "evaluation_scope": {
15020
16209
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
15021
16210
  "type": "string",
@@ -15119,7 +16308,7 @@ const operationManifest = [
15119
16308
  "questions": {
15120
16309
  "type": "array",
15121
16310
  "items": { "type": "string" },
15122
- "description": "Yes/no questions asked once the conditions match; all must be yes."
16311
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
15123
16312
  }
15124
16313
  },
15125
16314
  "required": [
@@ -15197,6 +16386,112 @@ const operationManifest = [
15197
16386
  }
15198
16387
  },
15199
16388
  "required": ["match", "no_match"]
16389
+ },
16390
+ "expectation_samples": {
16391
+ "description": "Its saved made-up emails, oldest first.",
16392
+ "type": "array",
16393
+ "items": {
16394
+ "type": "object",
16395
+ "properties": {
16396
+ "id": {
16397
+ "type": "string",
16398
+ "format": "uuid"
16399
+ },
16400
+ "expected": {
16401
+ "type": "string",
16402
+ "enum": ["match", "no_match"]
16403
+ },
16404
+ "label": { "type": ["string", "null"] },
16405
+ "email": {
16406
+ "type": "object",
16407
+ "properties": {
16408
+ "from": {
16409
+ "type": "object",
16410
+ "properties": {
16411
+ "address": {
16412
+ "type": "string",
16413
+ "maxLength": 254,
16414
+ "format": "email",
16415
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
16416
+ },
16417
+ "name": {
16418
+ "type": "string",
16419
+ "maxLength": 200,
16420
+ "pattern": "^[^\\u0000]*$"
16421
+ }
16422
+ },
16423
+ "required": ["address"]
16424
+ },
16425
+ "to": {
16426
+ "minItems": 1,
16427
+ "maxItems": 10,
16428
+ "type": "array",
16429
+ "items": {
16430
+ "type": "string",
16431
+ "maxLength": 254,
16432
+ "format": "email",
16433
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
16434
+ }
16435
+ },
16436
+ "cc": {
16437
+ "maxItems": 10,
16438
+ "type": "array",
16439
+ "items": {
16440
+ "type": "string",
16441
+ "maxLength": 254,
16442
+ "format": "email",
16443
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
16444
+ }
16445
+ },
16446
+ "subject": {
16447
+ "type": "string",
16448
+ "maxLength": 998,
16449
+ "pattern": "^[^\\u0000]*$"
16450
+ },
16451
+ "text": {
16452
+ "type": "string",
16453
+ "minLength": 1,
16454
+ "maxLength": 4e3,
16455
+ "pattern": "^[^\\u0000]*$"
16456
+ },
16457
+ "attachment_types": {
16458
+ "maxItems": 10,
16459
+ "type": "array",
16460
+ "items": {
16461
+ "type": "string",
16462
+ "minLength": 1,
16463
+ "maxLength": 255,
16464
+ "pattern": "^[^\\u0000]*$"
16465
+ }
16466
+ },
16467
+ "automated": { "type": "boolean" },
16468
+ "from_mailing_list": { "type": "boolean" },
16469
+ "spam_score": {
16470
+ "type": "number",
16471
+ "minimum": -1e3,
16472
+ "maximum": 1e3
16473
+ }
16474
+ },
16475
+ "required": [
16476
+ "from",
16477
+ "to",
16478
+ "text"
16479
+ ],
16480
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
16481
+ },
16482
+ "created_at": {
16483
+ "type": "string",
16484
+ "format": "date-time"
16485
+ }
16486
+ },
16487
+ "required": [
16488
+ "id",
16489
+ "expected",
16490
+ "label",
16491
+ "email",
16492
+ "created_at"
16493
+ ]
16494
+ }
15200
16495
  }
15201
16496
  },
15202
16497
  "required": [
@@ -15220,7 +16515,7 @@ const operationManifest = [
15220
16515
  "binaryResponse": false,
15221
16516
  "bodyRequired": false,
15222
16517
  "command": "get-email-rule-request",
15223
- "description": "A rule request: the rule as proposed, who made it and to whom, and whether each of them is still a member. A request is visible to the member who made it, the member it was made to, and the organization's owners and admins. If you are the member asked, you can try the rule as proposed before answering: pass the request's tree to POST /v1/email-rules/test to test it on mail you have received. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up. Nothing is stored, and it needs no access beyond testing a rule of your own. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
16518
+ "description": "A rule request: the rule as proposed, who made it and to whom, and whether each of them is still a member. A request is visible to the member who made it, the member it was made to, and the organization's owners and admins. If you are the member asked, you can try the rule as proposed before answering: pass the request's tree to POST /v1/email-rules/test to test it on mail you have received. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up, or have look-alikes written for it (POST /v1/email-rules/look-alikes with request_id; primitive email-rules generate-look-alikes). Neither stores anything, and neither needs access beyond testing a rule of your own; the accept keeps any you send as expectation_samples on the rule it writes. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
15224
16519
  "hasJsonBody": false,
15225
16520
  "method": "GET",
15226
16521
  "operationId": "getEmailRuleRequest",
@@ -15275,7 +16570,7 @@ const operationManifest = [
15275
16570
  "questions": {
15276
16571
  "type": "array",
15277
16572
  "items": { "type": "string" },
15278
- "description": "Yes/no questions asked once the conditions match; all must be yes."
16573
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
15279
16574
  }
15280
16575
  },
15281
16576
  "required": [
@@ -15384,6 +16679,14 @@ const operationManifest = [
15384
16679
  "decline_reason": {
15385
16680
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
15386
16681
  "type": ["string", "null"]
16682
+ },
16683
+ "edited_on_accept": {
16684
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
16685
+ "type": ["boolean", "null"]
16686
+ },
16687
+ "notice": {
16688
+ "description": "For the member asked, while pending: what accepting would share. For the member who asked, once accepted with changes: that the request is not what is shared. Else null.",
16689
+ "type": ["string", "null"]
15387
16690
  }
15388
16691
  },
15389
16692
  "required": [
@@ -15974,7 +17277,7 @@ const operationManifest = [
15974
17277
  "matched": { "type": "boolean" },
15975
17278
  "near_miss": {
15976
17279
  "type": "boolean",
15977
- "description": "Not matched, but nearly: its questions did not all say yes, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false). Decisions recorded under the earlier definition counted an exclusion too."
17280
+ "description": "Not matched, but nearly: its conditions held and its questions did not match it, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false). Decisions recorded under the earlier definition counted an exclusion too."
15978
17281
  },
15979
17282
  "parts": {
15980
17283
  "type": "array",
@@ -16157,7 +17460,7 @@ const operationManifest = [
16157
17460
  "questions": {
16158
17461
  "type": "array",
16159
17462
  "items": { "type": "string" },
16160
- "description": "Yes/no questions asked once the conditions match; all must be yes."
17463
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
16161
17464
  }
16162
17465
  },
16163
17466
  "required": [
@@ -16266,6 +17569,14 @@ const operationManifest = [
16266
17569
  "decline_reason": {
16267
17570
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
16268
17571
  "type": ["string", "null"]
17572
+ },
17573
+ "edited_on_accept": {
17574
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
17575
+ "type": ["boolean", "null"]
17576
+ },
17577
+ "notice": {
17578
+ "description": "For the member asked, while pending: what accepting would share. For the member who asked, once accepted with changes: that the request is not what is shared. Else null.",
17579
+ "type": ["string", "null"]
16269
17580
  }
16270
17581
  },
16271
17582
  "required": [
@@ -16301,7 +17612,7 @@ const operationManifest = [
16301
17612
  "binaryResponse": false,
16302
17613
  "bodyRequired": false,
16303
17614
  "command": "list-email-rule-results",
16304
- "description": "Every rule you can see and what it decided about this email (matched, not matched, or not run, with what is known), in the words of the version that decided. A matched do_not_share rule keeps rules from sharing the email while it is active; what was shared before then stays shared. Only a caller who can read the email gets an answer. classification is what the classifier read from the email, every fact as rule conditions read it, so you can see why a rule did or did not match. At most 50 rules are listed, those with a result first; hidden counts the rest. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
17615
+ "description": "What each rule you can see decided about this email (matched, not matched, or not run), in the words of the deciding version. A do_not_share rule that matched it blocks sharing while active; earlier shares stay. Only a caller who can read the email gets an answer. classification is every fact the classifier read, as rule conditions read it. At most 50 rules are listed, results first; not_listed counts the rest. Another member's rule that decided on your own mail is listed read_only (dashboard and CLI only); hidden counts deciding rules you cannot see. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
16305
17616
  "hasJsonBody": false,
16306
17617
  "method": "GET",
16307
17618
  "operationId": "listEmailRuleResults",
@@ -16360,7 +17671,7 @@ const operationManifest = [
16360
17671
  "type": "string",
16361
17672
  "format": "uuid"
16362
17673
  },
16363
- "name": { "type": "string" },
17674
+ "name": { "type": ["string", "null"] },
16364
17675
  "state": {
16365
17676
  "type": "string",
16366
17677
  "enum": [
@@ -16384,7 +17695,7 @@ const operationManifest = [
16384
17695
  },
16385
17696
  "matched": { "type": "boolean" },
16386
17697
  "near_miss": {
16387
- "description": "Not matched, but nearly: its questions did not all say yes, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false). False when detail_withheld is true. Decisions recorded under the earlier definition counted an exclusion too.",
17698
+ "description": "Not matched, but nearly: its conditions held and its questions did not match it, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false). False when detail_withheld is true. Decisions recorded under the earlier definition counted an exclusion too.",
16388
17699
  "type": "boolean"
16389
17700
  },
16390
17701
  "parts": {
@@ -16450,7 +17761,7 @@ const operationManifest = [
16450
17761
  "questions": {
16451
17762
  "type": "array",
16452
17763
  "items": { "type": "string" },
16453
- "description": "Yes/no questions asked once the conditions match; all must be yes."
17764
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
16454
17765
  }
16455
17766
  },
16456
17767
  "required": [
@@ -16527,6 +17838,33 @@ const operationManifest = [
16527
17838
  "description": "On a do-not-share rule whose author is no longer a member of the organization: it blocks nothing now, whatever it decided. Absent otherwise.",
16528
17839
  "type": "boolean",
16529
17840
  "const": true
17841
+ },
17842
+ "read_only": {
17843
+ "description": "Another member's rule that decided on your own mail; dashboard and CLI only. name is null once it no longer covers you.",
17844
+ "type": "boolean",
17845
+ "const": true
17846
+ },
17847
+ "author": {
17848
+ "type": "object",
17849
+ "properties": {
17850
+ "user_id": {
17851
+ "type": "string",
17852
+ "format": "uuid"
17853
+ },
17854
+ "name": {
17855
+ "description": "Their name; null when they have none set.",
17856
+ "type": ["string", "null"]
17857
+ },
17858
+ "email": {
17859
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
17860
+ "type": ["string", "null"]
17861
+ }
17862
+ },
17863
+ "required": [
17864
+ "user_id",
17865
+ "name",
17866
+ "email"
17867
+ ]
16530
17868
  }
16531
17869
  },
16532
17870
  "required": [
@@ -16537,12 +17875,17 @@ const operationManifest = [
16537
17875
  "not_run_reason"
16538
17876
  ]
16539
17877
  },
16540
- "description": "Every rule the caller can see: those that matched this email, then those that did not, then those that have not run on it; newest rule first within each."
17878
+ "description": "Rules you can see, and read_only ones: matched first, then not matched, then not run; newest first within each."
16541
17879
  },
16542
17880
  "hidden": {
16543
17881
  "type": "integer",
16544
17882
  "minimum": 0,
16545
- "description": "How many more rules the caller can see that are not listed."
17883
+ "description": "Rules that decided on this email that you cannot see."
17884
+ },
17885
+ "not_listed": {
17886
+ "description": "Rules you can see past the 50 listed.",
17887
+ "type": "integer",
17888
+ "minimum": 0
16546
17889
  },
16547
17890
  "classification": {
16548
17891
  "type": ["object", "null"],
@@ -17283,7 +18626,7 @@ const operationManifest = [
17283
18626
  "questions": {
17284
18627
  "type": "array",
17285
18628
  "items": { "type": "string" },
17286
- "description": "Yes/no questions asked once the conditions match; all must be yes."
18629
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
17287
18630
  }
17288
18631
  },
17289
18632
  "required": [
@@ -17409,6 +18752,10 @@ const operationManifest = [
17409
18752
  "type": "string",
17410
18753
  "enum": ["grant", "do_not_share"]
17411
18754
  },
18755
+ "protection_note": {
18756
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), a classifier fact narrows it, or its several questions are alternatives.",
18757
+ "type": "string"
18758
+ },
17412
18759
  "evaluation_scope": {
17413
18760
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
17414
18761
  "type": "string",
@@ -17751,7 +19098,7 @@ const operationManifest = [
17751
19098
  "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
17752
19099
  },
17753
19100
  "questions": {
17754
- "description": "Yes/no questions asked once the conditions match; all must be yes. Left out, none.",
19101
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes. Left out, none.",
17755
19102
  "type": "array",
17756
19103
  "items": { "type": "string" }
17757
19104
  }
@@ -17841,10 +19188,114 @@ const operationManifest = [
17841
19188
  "type": "string",
17842
19189
  "format": "uuid"
17843
19190
  }
19191
+ },
19192
+ "samples": {
19193
+ "maxItems": 20,
19194
+ "type": "array",
19195
+ "items": {
19196
+ "type": "object",
19197
+ "properties": {
19198
+ "expected": {
19199
+ "type": "string",
19200
+ "enum": ["match", "no_match"]
19201
+ },
19202
+ "label": {
19203
+ "type": ["string", "null"],
19204
+ "minLength": 1,
19205
+ "maxLength": 200,
19206
+ "pattern": "^[^\\u0000]*$",
19207
+ "description": "What the email stands for, such as a personal medical bill."
19208
+ },
19209
+ "email": {
19210
+ "type": "object",
19211
+ "properties": {
19212
+ "from": {
19213
+ "type": "object",
19214
+ "properties": {
19215
+ "address": {
19216
+ "type": "string",
19217
+ "maxLength": 254,
19218
+ "format": "email",
19219
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
19220
+ },
19221
+ "name": {
19222
+ "type": "string",
19223
+ "maxLength": 200,
19224
+ "pattern": "^[^\\u0000]*$"
19225
+ }
19226
+ },
19227
+ "required": ["address"]
19228
+ },
19229
+ "to": {
19230
+ "minItems": 1,
19231
+ "maxItems": 10,
19232
+ "type": "array",
19233
+ "items": {
19234
+ "type": "string",
19235
+ "maxLength": 254,
19236
+ "format": "email",
19237
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
19238
+ }
19239
+ },
19240
+ "cc": {
19241
+ "maxItems": 10,
19242
+ "type": "array",
19243
+ "items": {
19244
+ "type": "string",
19245
+ "maxLength": 254,
19246
+ "format": "email",
19247
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
19248
+ }
19249
+ },
19250
+ "subject": {
19251
+ "type": "string",
19252
+ "maxLength": 998,
19253
+ "pattern": "^[^\\u0000]*$"
19254
+ },
19255
+ "text": {
19256
+ "type": "string",
19257
+ "minLength": 1,
19258
+ "maxLength": 4e3,
19259
+ "pattern": "^[^\\u0000]*$"
19260
+ },
19261
+ "attachment_types": {
19262
+ "maxItems": 10,
19263
+ "type": "array",
19264
+ "items": {
19265
+ "type": "string",
19266
+ "minLength": 1,
19267
+ "maxLength": 255,
19268
+ "pattern": "^[^\\u0000]*$"
19269
+ }
19270
+ },
19271
+ "automated": { "type": "boolean" },
19272
+ "from_mailing_list": { "type": "boolean" },
19273
+ "spam_score": {
19274
+ "type": "number",
19275
+ "minimum": -1e3,
19276
+ "maximum": 1e3
19277
+ }
19278
+ },
19279
+ "required": [
19280
+ "from",
19281
+ "to",
19282
+ "text"
19283
+ ],
19284
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
19285
+ },
19286
+ "id": {
19287
+ "type": "string",
19288
+ "format": "uuid"
19289
+ },
19290
+ "created_at": { "type": "string" }
19291
+ },
19292
+ "required": ["expected", "email"],
19293
+ "additionalProperties": false
19294
+ }
17844
19295
  }
17845
19296
  },
17846
19297
  "additionalProperties": false,
17847
- "description": "Emails the rule must match and must not, at most 100 in all, always tested whatever their age: added to email_ids, or to the recent window, which gives up a place for each. The answer's expectations judge each."
19298
+ "description": "Emails the rule must match and must not (at most 100), always tested whatever their age, and made-up emails (samples, at most 20). Given, it replaces a saved rule's own."
17848
19299
  }
17849
19300
  },
17850
19301
  "additionalProperties": false
@@ -17991,7 +19442,7 @@ const operationManifest = [
17991
19442
  "skipped",
17992
19443
  "not_found"
17993
19444
  ],
17994
- "description": "near_miss: Not matched, but nearly: its questions did not all say yes, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false). skipped: an email you can see that cannot be tested; skipped_reason says why. not_found: no such email, or not one the rule can read (or not settled, or awaiting sender authentication)."
19445
+ "description": "near_miss: Not matched, but nearly: its conditions held and its questions did not match it, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false). skipped: an email you can see that cannot be tested; skipped_reason says why. not_found: no such email, or not one the rule can read (or not settled, or awaiting sender authentication)."
17995
19446
  },
17996
19447
  "received_at": {
17997
19448
  "type": "string",
@@ -18226,7 +19677,7 @@ const operationManifest = [
18226
19677
  }
18227
19678
  },
18228
19679
  "expectations": {
18229
- "description": "Present when expect was given, or for a saved rule with saved expectations.",
19680
+ "description": "Present when expect was given, or for a saved rule with saved expectations or made-up emails.",
18230
19681
  "type": "object",
18231
19682
  "properties": {
18232
19683
  "passed": {
@@ -18296,6 +19747,71 @@ const operationManifest = [
18296
19747
  ]
18297
19748
  },
18298
19749
  "description": "match first, then no_match, each in the order given (saved expectations by email id)."
19750
+ },
19751
+ "samples": {
19752
+ "description": "Made-up emails judged (expect.samples, or a saved rule's), in order; counts include them.",
19753
+ "type": "array",
19754
+ "items": {
19755
+ "type": "object",
19756
+ "properties": {
19757
+ "index": {
19758
+ "type": "integer",
19759
+ "minimum": 0
19760
+ },
19761
+ "sample_id": {
19762
+ "type": ["string", "null"],
19763
+ "format": "uuid"
19764
+ },
19765
+ "label": { "type": ["string", "null"] },
19766
+ "expected": {
19767
+ "type": "string",
19768
+ "enum": ["match", "no_match"]
19769
+ },
19770
+ "outcome": {
19771
+ "type": "string",
19772
+ "enum": [
19773
+ "matched",
19774
+ "near_miss",
19775
+ "no_match",
19776
+ "not_decided"
19777
+ ]
19778
+ },
19779
+ "status": {
19780
+ "type": "string",
19781
+ "enum": [
19782
+ "passed",
19783
+ "failed",
19784
+ "undecided"
19785
+ ]
19786
+ },
19787
+ "question_answers": {
19788
+ "type": "array",
19789
+ "items": {
19790
+ "type": "object",
19791
+ "properties": {
19792
+ "question": { "type": "string" },
19793
+ "answer": { "type": ["boolean", "null"] },
19794
+ "probability": { "type": ["number", "null"] },
19795
+ "error_class": { "type": "string" }
19796
+ },
19797
+ "required": [
19798
+ "question",
19799
+ "answer",
19800
+ "probability"
19801
+ ]
19802
+ }
19803
+ }
19804
+ },
19805
+ "required": [
19806
+ "index",
19807
+ "sample_id",
19808
+ "label",
19809
+ "expected",
19810
+ "outcome",
19811
+ "status",
19812
+ "question_answers"
19813
+ ]
19814
+ }
18299
19815
  }
18300
19816
  },
18301
19817
  "required": [
@@ -18360,7 +19876,7 @@ const operationManifest = [
18360
19876
  "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
18361
19877
  },
18362
19878
  "questions": {
18363
- "description": "Yes/no questions asked once the conditions match; all must be yes. Left out, none.",
19879
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes. Left out, none.",
18364
19880
  "type": "array",
18365
19881
  "items": { "type": "string" }
18366
19882
  }
@@ -18541,7 +20057,7 @@ const operationManifest = [
18541
20057
  "near_miss",
18542
20058
  "no_match"
18543
20059
  ],
18544
- "description": "near_miss: Not matched, but nearly: its questions did not all say yes, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false)."
20060
+ "description": "near_miss: Not matched, but nearly: its conditions held and its questions did not match it, or it needs several parts and exactly one failed while at least one part that held is not an exclusion (a top-level not, or is_false)."
18545
20061
  },
18546
20062
  "parts": {
18547
20063
  "type": "array",
@@ -18728,7 +20244,7 @@ const operationManifest = [
18728
20244
  "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
18729
20245
  },
18730
20246
  "questions": {
18731
- "description": "Yes/no questions asked once the conditions match; all must be yes. Left out, none.",
20247
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes. Left out, none.",
18732
20248
  "type": "array",
18733
20249
  "items": { "type": "string" }
18734
20250
  }
@@ -18855,6 +20371,217 @@ const operationManifest = [
18855
20371
  "additionalProperties": false,
18856
20372
  "description": "Replaces its saved expectations (author only); null removes them."
18857
20373
  },
20374
+ "expectation_samples": {
20375
+ "maxItems": 20,
20376
+ "type": ["array", "null"],
20377
+ "items": {
20378
+ "type": "object",
20379
+ "properties": {
20380
+ "expected": {
20381
+ "type": "string",
20382
+ "enum": ["match", "no_match"]
20383
+ },
20384
+ "label": {
20385
+ "type": ["string", "null"],
20386
+ "minLength": 1,
20387
+ "maxLength": 200,
20388
+ "pattern": "^[^\\u0000]*$",
20389
+ "description": "What the email stands for, such as a personal medical bill."
20390
+ },
20391
+ "email": {
20392
+ "type": "object",
20393
+ "properties": {
20394
+ "from": {
20395
+ "type": "object",
20396
+ "properties": {
20397
+ "address": {
20398
+ "type": "string",
20399
+ "maxLength": 254,
20400
+ "format": "email",
20401
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20402
+ },
20403
+ "name": {
20404
+ "type": "string",
20405
+ "maxLength": 200,
20406
+ "pattern": "^[^\\u0000]*$"
20407
+ }
20408
+ },
20409
+ "required": ["address"]
20410
+ },
20411
+ "to": {
20412
+ "minItems": 1,
20413
+ "maxItems": 10,
20414
+ "type": "array",
20415
+ "items": {
20416
+ "type": "string",
20417
+ "maxLength": 254,
20418
+ "format": "email",
20419
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20420
+ }
20421
+ },
20422
+ "cc": {
20423
+ "maxItems": 10,
20424
+ "type": "array",
20425
+ "items": {
20426
+ "type": "string",
20427
+ "maxLength": 254,
20428
+ "format": "email",
20429
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20430
+ }
20431
+ },
20432
+ "subject": {
20433
+ "type": "string",
20434
+ "maxLength": 998,
20435
+ "pattern": "^[^\\u0000]*$"
20436
+ },
20437
+ "text": {
20438
+ "type": "string",
20439
+ "minLength": 1,
20440
+ "maxLength": 4e3,
20441
+ "pattern": "^[^\\u0000]*$"
20442
+ },
20443
+ "attachment_types": {
20444
+ "maxItems": 10,
20445
+ "type": "array",
20446
+ "items": {
20447
+ "type": "string",
20448
+ "minLength": 1,
20449
+ "maxLength": 255,
20450
+ "pattern": "^[^\\u0000]*$"
20451
+ }
20452
+ },
20453
+ "automated": { "type": "boolean" },
20454
+ "from_mailing_list": { "type": "boolean" },
20455
+ "spam_score": {
20456
+ "type": "number",
20457
+ "minimum": -1e3,
20458
+ "maximum": 1e3
20459
+ }
20460
+ },
20461
+ "required": [
20462
+ "from",
20463
+ "to",
20464
+ "text"
20465
+ ],
20466
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
20467
+ },
20468
+ "id": {
20469
+ "type": "string",
20470
+ "format": "uuid"
20471
+ },
20472
+ "created_at": { "type": "string" }
20473
+ },
20474
+ "required": ["expected", "email"],
20475
+ "additionalProperties": false
20476
+ },
20477
+ "description": "Replaces its saved made-up emails (author only); null or [] removes them."
20478
+ },
20479
+ "add_expectation_samples": {
20480
+ "description": "Adds made-up emails to those saved (author only), up to 20 in all.",
20481
+ "minItems": 1,
20482
+ "maxItems": 20,
20483
+ "type": "array",
20484
+ "items": {
20485
+ "type": "object",
20486
+ "properties": {
20487
+ "expected": {
20488
+ "type": "string",
20489
+ "enum": ["match", "no_match"]
20490
+ },
20491
+ "label": {
20492
+ "type": ["string", "null"],
20493
+ "minLength": 1,
20494
+ "maxLength": 200,
20495
+ "pattern": "^[^\\u0000]*$",
20496
+ "description": "What the email stands for, such as a personal medical bill."
20497
+ },
20498
+ "email": {
20499
+ "type": "object",
20500
+ "properties": {
20501
+ "from": {
20502
+ "type": "object",
20503
+ "properties": {
20504
+ "address": {
20505
+ "type": "string",
20506
+ "maxLength": 254,
20507
+ "format": "email",
20508
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20509
+ },
20510
+ "name": {
20511
+ "type": "string",
20512
+ "maxLength": 200,
20513
+ "pattern": "^[^\\u0000]*$"
20514
+ }
20515
+ },
20516
+ "required": ["address"]
20517
+ },
20518
+ "to": {
20519
+ "minItems": 1,
20520
+ "maxItems": 10,
20521
+ "type": "array",
20522
+ "items": {
20523
+ "type": "string",
20524
+ "maxLength": 254,
20525
+ "format": "email",
20526
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20527
+ }
20528
+ },
20529
+ "cc": {
20530
+ "maxItems": 10,
20531
+ "type": "array",
20532
+ "items": {
20533
+ "type": "string",
20534
+ "maxLength": 254,
20535
+ "format": "email",
20536
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20537
+ }
20538
+ },
20539
+ "subject": {
20540
+ "type": "string",
20541
+ "maxLength": 998,
20542
+ "pattern": "^[^\\u0000]*$"
20543
+ },
20544
+ "text": {
20545
+ "type": "string",
20546
+ "minLength": 1,
20547
+ "maxLength": 4e3,
20548
+ "pattern": "^[^\\u0000]*$"
20549
+ },
20550
+ "attachment_types": {
20551
+ "maxItems": 10,
20552
+ "type": "array",
20553
+ "items": {
20554
+ "type": "string",
20555
+ "minLength": 1,
20556
+ "maxLength": 255,
20557
+ "pattern": "^[^\\u0000]*$"
20558
+ }
20559
+ },
20560
+ "automated": { "type": "boolean" },
20561
+ "from_mailing_list": { "type": "boolean" },
20562
+ "spam_score": {
20563
+ "type": "number",
20564
+ "minimum": -1e3,
20565
+ "maximum": 1e3
20566
+ }
20567
+ },
20568
+ "required": [
20569
+ "from",
20570
+ "to",
20571
+ "text"
20572
+ ],
20573
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
20574
+ },
20575
+ "id": {
20576
+ "type": "string",
20577
+ "format": "uuid"
20578
+ },
20579
+ "created_at": { "type": "string" }
20580
+ },
20581
+ "required": ["expected", "email"],
20582
+ "additionalProperties": false
20583
+ }
20584
+ },
18858
20585
  "if_revision": {
18859
20586
  "description": "The revision you read: if the rule was updated since, nothing is saved (409 email_rule_conflict, revision_moved).",
18860
20587
  "type": "integer",
@@ -18862,7 +20589,7 @@ const operationManifest = [
18862
20589
  "maximum": 2147483647
18863
20590
  },
18864
20591
  "if_expectations_pass": {
18865
- "description": "true: judge the rule as this write leaves it (new tree, restored version, new scope, or the version it activates) against the expectations it leaves, as POST /email-rules/test would, before saving; it can take as long as a test. Failed: 409 expectations_failed; undecided: 409 expectations_undecided (both with details.expectations_check); none to judge: 400 no_expectations. Refused writes save nothing; a pass saves unless the rule moved meanwhile (409 email_rule_conflict). Only writes that save a version or activate the rule are judged.",
20592
+ "description": "true: judge the rule as this write leaves it (new tree, restored version, new scope, or the version it activates) against the expectations it leaves, as POST /email-rules/test would, before saving; it can take as long as a test. Failed: 409 expectations_failed; on emails the rule cannot read: 409 expectations_unreadable; undecided: 409 expectations_undecided (each with details.expectations_check); none to judge: 400 no_expectations. Refused writes save nothing; a pass saves unless the rule moved meanwhile (409 email_rule_conflict). Only writes that save a version or activate the rule are judged.",
18866
20593
  "type": "boolean"
18867
20594
  },
18868
20595
  "restore_version": {
@@ -18895,6 +20622,10 @@ const operationManifest = [
18895
20622
  "type": "string",
18896
20623
  "enum": ["grant", "do_not_share"]
18897
20624
  },
20625
+ "protection_note": {
20626
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), a classifier fact narrows it, or its several questions are alternatives.",
20627
+ "type": "string"
20628
+ },
18898
20629
  "evaluation_scope": {
18899
20630
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
18900
20631
  "type": "string",
@@ -18998,7 +20729,7 @@ const operationManifest = [
18998
20729
  "questions": {
18999
20730
  "type": "array",
19000
20731
  "items": { "type": "string" },
19001
- "description": "Yes/no questions asked once the conditions match; all must be yes."
20732
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
19002
20733
  }
19003
20734
  },
19004
20735
  "required": [
@@ -19086,8 +20817,114 @@ const operationManifest = [
19086
20817
  ]
19087
20818
  }
19088
20819
  },
20820
+ "expectation_samples": {
20821
+ "description": "Its saved made-up emails, when this write set or added some.",
20822
+ "type": "array",
20823
+ "items": {
20824
+ "type": "object",
20825
+ "properties": {
20826
+ "id": {
20827
+ "type": "string",
20828
+ "format": "uuid"
20829
+ },
20830
+ "expected": {
20831
+ "type": "string",
20832
+ "enum": ["match", "no_match"]
20833
+ },
20834
+ "label": { "type": ["string", "null"] },
20835
+ "email": {
20836
+ "type": "object",
20837
+ "properties": {
20838
+ "from": {
20839
+ "type": "object",
20840
+ "properties": {
20841
+ "address": {
20842
+ "type": "string",
20843
+ "maxLength": 254,
20844
+ "format": "email",
20845
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20846
+ },
20847
+ "name": {
20848
+ "type": "string",
20849
+ "maxLength": 200,
20850
+ "pattern": "^[^\\u0000]*$"
20851
+ }
20852
+ },
20853
+ "required": ["address"]
20854
+ },
20855
+ "to": {
20856
+ "minItems": 1,
20857
+ "maxItems": 10,
20858
+ "type": "array",
20859
+ "items": {
20860
+ "type": "string",
20861
+ "maxLength": 254,
20862
+ "format": "email",
20863
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20864
+ }
20865
+ },
20866
+ "cc": {
20867
+ "maxItems": 10,
20868
+ "type": "array",
20869
+ "items": {
20870
+ "type": "string",
20871
+ "maxLength": 254,
20872
+ "format": "email",
20873
+ "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
20874
+ }
20875
+ },
20876
+ "subject": {
20877
+ "type": "string",
20878
+ "maxLength": 998,
20879
+ "pattern": "^[^\\u0000]*$"
20880
+ },
20881
+ "text": {
20882
+ "type": "string",
20883
+ "minLength": 1,
20884
+ "maxLength": 4e3,
20885
+ "pattern": "^[^\\u0000]*$"
20886
+ },
20887
+ "attachment_types": {
20888
+ "maxItems": 10,
20889
+ "type": "array",
20890
+ "items": {
20891
+ "type": "string",
20892
+ "minLength": 1,
20893
+ "maxLength": 255,
20894
+ "pattern": "^[^\\u0000]*$"
20895
+ }
20896
+ },
20897
+ "automated": { "type": "boolean" },
20898
+ "from_mailing_list": { "type": "boolean" },
20899
+ "spam_score": {
20900
+ "type": "number",
20901
+ "minimum": -1e3,
20902
+ "maximum": 1e3
20903
+ }
20904
+ },
20905
+ "required": [
20906
+ "from",
20907
+ "to",
20908
+ "text"
20909
+ ],
20910
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
20911
+ },
20912
+ "created_at": {
20913
+ "type": "string",
20914
+ "format": "date-time"
20915
+ }
20916
+ },
20917
+ "required": [
20918
+ "id",
20919
+ "expected",
20920
+ "label",
20921
+ "email",
20922
+ "created_at"
20923
+ ]
20924
+ }
20925
+ },
19089
20926
  "expectations_check": {
19090
- "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
20927
+ "description": "After a write that saved a version or changed its expectations or made-up emails, if it has some: the emails by id judged on that version from stored classifications without asking its questions, and any made-up emails listed as undecided. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed), and after a write that changed only made-up emails on a rule with emails by id. Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
19091
20928
  "type": "object",
19092
20929
  "properties": {
19093
20930
  "version": {
@@ -19099,7 +20936,7 @@ const operationManifest = [
19099
20936
  "type": ["boolean", "null"]
19100
20937
  },
19101
20938
  "decided": {
19102
- "description": "Whether every expectation was judged (undecided is empty). Absent from a server that predates it.",
20939
+ "description": "Whether every expectation was judged (undecided and undecided_samples are empty). Absent from a server that predates it.",
19103
20940
  "type": "boolean"
19104
20941
  },
19105
20942
  "failed": {
@@ -19116,8 +20953,24 @@ const operationManifest = [
19116
20953
  "format": "uuid"
19117
20954
  }
19118
20955
  },
20956
+ "failed_samples": {
20957
+ "description": "Made-up emails that failed, by index in the set judged (saved oldest first, then added; or as sent).",
20958
+ "type": "array",
20959
+ "items": {
20960
+ "type": "integer",
20961
+ "minimum": 0
20962
+ }
20963
+ },
20964
+ "undecided_samples": {
20965
+ "description": "Made-up emails not decided, by index. Only a write sent with if_expectations_pass that saves a version or activates the rule judges them.",
20966
+ "type": "array",
20967
+ "items": {
20968
+ "type": "integer",
20969
+ "minimum": 0
20970
+ }
20971
+ },
19119
20972
  "note": {
19120
- "description": "On an update that saved a version that did not pass: that it was saved anyway, is current, and whether it is live.",
20973
+ "description": "On an update that saved a version that did not pass, or a do_not_share rule created active: that it was saved anyway, is current, and whether it is live.",
19121
20974
  "type": "string"
19122
20975
  }
19123
20976
  },
@@ -19246,7 +21099,7 @@ const operationManifest = [
19246
21099
  "questions": {
19247
21100
  "type": "array",
19248
21101
  "items": { "type": "string" },
19249
- "description": "Yes/no questions asked once the conditions match; all must be yes."
21102
+ "description": "Yes/no questions asked once the conditions match, over the email's text and attachment names, never attachment contents. A grant rule needs every one yes; a do_not_share rule matches on any yes."
19250
21103
  }
19251
21104
  },
19252
21105
  "required": [
@@ -19355,6 +21208,14 @@ const operationManifest = [
19355
21208
  "decline_reason": {
19356
21209
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
19357
21210
  "type": ["string", "null"]
21211
+ },
21212
+ "edited_on_accept": {
21213
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
21214
+ "type": ["boolean", "null"]
21215
+ },
21216
+ "notice": {
21217
+ "description": "For the member asked, while pending: what accepting would share. For the member who asked, once accepted with changes: that the request is not what is shared. Else null.",
21218
+ "type": ["string", "null"]
19358
21219
  }
19359
21220
  },
19360
21221
  "required": [
@@ -19892,12 +21753,12 @@ const operationManifest = [
19892
21753
  },
19893
21754
  "hold": {
19894
21755
  "type": "object",
19895
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
21756
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
19896
21757
  "required": ["reason", "since"],
19897
21758
  "properties": {
19898
21759
  "reason": {
19899
21760
  "type": "string",
19900
- "description": "Open: credential_reuse_hold."
21761
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
19901
21762
  },
19902
21763
  "since": {
19903
21764
  "type": ["string", "null"],
@@ -35371,7 +37232,7 @@ const operationManifest = [
35371
37232
  "binaryResponse": false,
35372
37233
  "bodyRequired": true,
35373
37234
  "command": "decide-suspension",
35374
- "description": "Keep, narrow or revoke one suspended or flagged item. Keeping it, and revoking an API key, needs the decider to cover it (revoking an uncovered key is refused with `grant_forbidden`, `target_not_covered`). Keeping an API key rotated (`rotate: true`, other than the shared Function key) also revokes its older secrets still in a rotation overlap, and needs keys:manage on both the credential and the person: without it, or if the decider lost members:manage meanwhile, it is refused with `grant_forbidden` (`keys_manage_required` or `actor_access_changed`) and nothing is rotated. A rule share paused by a role change (`email_rule_share`) takes keep, which leaves it paused, or revoke, which ends the share and needs confirm: true (the person it was shared with loses the emails already shared; without it, 409 `confirmation_required`, as for revoking a filter). Neither of the share's two people may keep it (`grant_forbidden`, `share_decision_forbidden`). Access a person gave on their own mailbox (`mailbox_access`) takes keep or revoke, and both need delegations:manage; revoke removes only what that person gave on that mailbox. Nobody the access reaches, nor the person who gave it, may keep it (`grant_forbidden`, `mailbox_access_decision_forbidden`). Person only: a dashboard session, or the person's own OAuth or CLI token with a session step-up of purpose `suspensions.decide` (the person confirms at the step_up_url a 403 step_up_required answer carries, which names this client, then send the token in the Primitive-Step-Up header; the token works only with the client it was confirmed for). Needs members:manage. API keys are refused.",
37235
+ "description": "Keep, narrow or revoke one suspended or flagged item. Keeping it needs the decider to cover it, and so does revoking an API key unless the decider holds it and decides as themself rather than through another app (revoking an uncovered key someone else holds is refused with `grant_forbidden`, `target_not_covered`). Keeping an API key rotated (`rotate: true`, other than the shared Function key) also revokes its older secrets still in a rotation overlap, and needs keys:manage on both the credential and the person: without it, or if the decider lost members:manage meanwhile, it is refused with `grant_forbidden` (`keys_manage_required` or `actor_access_changed`) and nothing is rotated. A rule share paused by a role change (`email_rule_share`) takes keep, which leaves it paused, or revoke, which ends the share and needs confirm: true (the person it was shared with loses the emails already shared; without it, 409 `confirmation_required`, as for revoking a filter). Neither of the share's two people may keep it (`grant_forbidden`, `share_decision_forbidden`). Access a person gave on their own mailbox (`mailbox_access`) takes keep or revoke, and both need delegations:manage; revoke removes only what that person gave on that mailbox. Nobody the access reaches, nor the person who gave it, may keep it (`grant_forbidden`, `mailbox_access_decision_forbidden`). Person only: a dashboard session, or the person's own OAuth or CLI token with a session step-up of purpose `suspensions.decide` (the person confirms at the step_up_url a 403 step_up_required answer carries, which names this client, then send the token in the Primitive-Step-Up header; the token works only with the client it was confirmed for). Needs members:manage. API keys are refused.",
35375
37236
  "hasJsonBody": true,
35376
37237
  "method": "POST",
35377
37238
  "operationId": "decideSuspension",
@@ -35636,7 +37497,7 @@ const operationManifest = [
35636
37497
  "binaryResponse": false,
35637
37498
  "bodyRequired": false,
35638
37499
  "command": "get-access-mode",
35639
- "description": "Whole-org or delegated mailbox access. Holders of roles:assign also get the preview of a switch (every item that needs a decision, each with the decision_options it takes, worded for what the switch does) and the decisions made, plus notices: member and group domain entries that delegated mode keeps off personal mailboxes, each with how many it leaves out. Notices are informational, never decided, and never hold the switch (notices_truncated past 500, notices_unavailable when they could not be read). In whole-org mode it also lists narrowed_agents: every live agent connection the switch narrows to an own-address key (personal agents of members, and shared agents), informational in the same way (narrowed_agents_truncated past 500, narrowed_agents_unavailable when they could not be read). Any person; API keys are refused.",
37500
+ "description": "Whole-org or delegated mailbox access. Holders of roles:assign also get the preview of a switch (every item that needs a decision, each with the decision_options it takes, worded for what the switch does) and the decisions made, plus notices: member and group domain entries that delegated mode keeps off personal mailboxes, each with how many it leaves out. Notices are informational, never decided, and never hold the switch (notices_truncated past 500, notices_unavailable when they could not be read). In whole-org mode it also lists narrowed_agents: every live agent connection the switch narrows to an own-address key (personal agents of members, and shared agents), informational in the same way (narrowed_agents_truncated past 500, narrowed_agents_unavailable when they could not be read). Needs members:read (owners, admins, members and roles that grant it); API keys are refused.",
35640
37501
  "hasJsonBody": false,
35641
37502
  "method": "GET",
35642
37503
  "operationId": "getAccessMode",
@@ -35898,7 +37759,7 @@ const operationManifest = [
35898
37759
  "binaryResponse": false,
35899
37760
  "bodyRequired": false,
35900
37761
  "command": "list-roles",
35901
- "description": "Every role in the organization (built-in, template copies and custom roles), with how many members and groups hold each, the templates not yet copied, and whether roles can be assigned now. `unassigned` describes role restricted on a member with no role_id, which is not a role and is never in `roles`. Any person (a session or OAuth token); API keys are refused.",
37762
+ "description": "Every role in the organization (built-in, template copies and custom roles), with how many members and groups hold each, the templates not yet copied, and whether roles can be assigned now. `unassigned` describes role restricted on a member with no role_id, which is not a role and is never in `roles`. Needs members:read (owners, admins, members and roles that grant it); API keys are refused.",
35902
37763
  "hasJsonBody": false,
35903
37764
  "method": "GET",
35904
37765
  "operationId": "listRoles",
@@ -38442,12 +40303,12 @@ const operationManifest = [
38442
40303
  },
38443
40304
  "hold": {
38444
40305
  "type": "object",
38445
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
40306
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
38446
40307
  "required": ["reason", "since"],
38447
40308
  "properties": {
38448
40309
  "reason": {
38449
40310
  "type": "string",
38450
- "description": "Open: credential_reuse_hold."
40311
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
38451
40312
  },
38452
40313
  "since": {
38453
40314
  "type": ["string", "null"],
@@ -38675,12 +40536,12 @@ const operationManifest = [
38675
40536
  },
38676
40537
  "hold": {
38677
40538
  "type": "object",
38678
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
40539
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
38679
40540
  "required": ["reason", "since"],
38680
40541
  "properties": {
38681
40542
  "reason": {
38682
40543
  "type": "string",
38683
- "description": "Open: credential_reuse_hold."
40544
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
38684
40545
  },
38685
40546
  "since": {
38686
40547
  "type": ["string", "null"],
@@ -39140,12 +41001,12 @@ const operationManifest = [
39140
41001
  },
39141
41002
  "hold": {
39142
41003
  "type": "object",
39143
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
41004
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
39144
41005
  "required": ["reason", "since"],
39145
41006
  "properties": {
39146
41007
  "reason": {
39147
41008
  "type": "string",
39148
- "description": "Open: credential_reuse_hold."
41009
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
39149
41010
  },
39150
41011
  "since": {
39151
41012
  "type": ["string", "null"],
@@ -39373,12 +41234,12 @@ const operationManifest = [
39373
41234
  },
39374
41235
  "hold": {
39375
41236
  "type": "object",
39376
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
41237
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
39377
41238
  "required": ["reason", "since"],
39378
41239
  "properties": {
39379
41240
  "reason": {
39380
41241
  "type": "string",
39381
- "description": "Open: credential_reuse_hold."
41242
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
39382
41243
  },
39383
41244
  "since": {
39384
41245
  "type": ["string", "null"],
@@ -40354,12 +42215,12 @@ const operationManifest = [
40354
42215
  },
40355
42216
  "hold": {
40356
42217
  "type": "object",
40357
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
42218
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
40358
42219
  "required": ["reason", "since"],
40359
42220
  "properties": {
40360
42221
  "reason": {
40361
42222
  "type": "string",
40362
- "description": "Open: credential_reuse_hold."
42223
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
40363
42224
  },
40364
42225
  "since": {
40365
42226
  "type": ["string", "null"],
@@ -40587,12 +42448,12 @@ const operationManifest = [
40587
42448
  },
40588
42449
  "hold": {
40589
42450
  "type": "object",
40590
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
42451
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
40591
42452
  "required": ["reason", "since"],
40592
42453
  "properties": {
40593
42454
  "reason": {
40594
42455
  "type": "string",
40595
- "description": "Open: credential_reuse_hold."
42456
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
40596
42457
  },
40597
42458
  "since": {
40598
42459
  "type": ["string", "null"],
@@ -41219,12 +43080,12 @@ const operationManifest = [
41219
43080
  },
41220
43081
  "hold": {
41221
43082
  "type": "object",
41222
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
43083
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
41223
43084
  "required": ["reason", "since"],
41224
43085
  "properties": {
41225
43086
  "reason": {
41226
43087
  "type": "string",
41227
- "description": "Open: credential_reuse_hold."
43088
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
41228
43089
  },
41229
43090
  "since": {
41230
43091
  "type": ["string", "null"],
@@ -41704,12 +43565,12 @@ const operationManifest = [
41704
43565
  },
41705
43566
  "hold": {
41706
43567
  "type": "object",
41707
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
43568
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
41708
43569
  "required": ["reason", "since"],
41709
43570
  "properties": {
41710
43571
  "reason": {
41711
43572
  "type": "string",
41712
- "description": "Open: credential_reuse_hold."
43573
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
41713
43574
  },
41714
43575
  "since": {
41715
43576
  "type": ["string", "null"],
@@ -41937,12 +43798,12 @@ const operationManifest = [
41937
43798
  },
41938
43799
  "hold": {
41939
43800
  "type": "object",
41940
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
43801
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
41941
43802
  "required": ["reason", "since"],
41942
43803
  "properties": {
41943
43804
  "reason": {
41944
43805
  "type": "string",
41945
- "description": "Open: credential_reuse_hold."
43806
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
41946
43807
  },
41947
43808
  "since": {
41948
43809
  "type": ["string", "null"],
@@ -42725,12 +44586,12 @@ const operationManifest = [
42725
44586
  },
42726
44587
  "hold": {
42727
44588
  "type": "object",
42728
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
44589
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
42729
44590
  "required": ["reason", "since"],
42730
44591
  "properties": {
42731
44592
  "reason": {
42732
44593
  "type": "string",
42733
- "description": "Open: credential_reuse_hold."
44594
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
42734
44595
  },
42735
44596
  "since": {
42736
44597
  "type": ["string", "null"],
@@ -42958,12 +44819,12 @@ const operationManifest = [
42958
44819
  },
42959
44820
  "hold": {
42960
44821
  "type": "object",
42961
- "description": "Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.",
44822
+ "description": "Only on a scheduled send that is not going out yet; status stays scheduled. credential_reuse_hold: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. sending_ip_warmup_cap: your dedicated sending address is at today's warm-up limit or paused, so it goes out on its own within the hour after the limit resets at 00:00 UTC (fails with sending_ip_warmup_cap after 7 such holds); no release needed. On sent-email reads and conversation messages.",
42962
44823
  "required": ["reason", "since"],
42963
44824
  "properties": {
42964
44825
  "reason": {
42965
44826
  "type": "string",
42966
- "description": "Open: credential_reuse_hold."
44827
+ "description": "Open: credential_reuse_hold, sending_ip_warmup_cap."
42967
44828
  },
42968
44829
  "since": {
42969
44830
  "type": ["string", "null"],
@@ -46675,7 +48536,10 @@ const operationManifest = [
46675
48536
  "format": "uuid",
46676
48537
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
46677
48538
  },
46678
- "endpoint_url": { "type": "string" },
48539
+ "endpoint_url": {
48540
+ "type": "string",
48541
+ "description": "Where it was sent: a webhook's scheme and host, followed by \"/... (path and query hidden)\" when its URL has more (GET /endpoints/{id} has the whole URL), function://<id> for a Function, or pull: for a pull subscription."
48542
+ },
46679
48543
  "status": { "type": "string" },
46680
48544
  "attempt_count": { "type": "integer" },
46681
48545
  "duration_ms": { "type": ["integer", "null"] },
@@ -46686,7 +48550,7 @@ const operationManifest = [
46686
48550
  "description": "The HTTP status the endpoint answered this attempt with: a webhook's own answer, or for a Function what its runtime answered, which is the platform's own error when the Function could not be run. Null when no answer came back (a network error, a timeout, an attempt refused or not sent) and on attempts recorded before this field."
46687
48551
  },
46688
48552
  "response_body": {
46689
- "description": "The first 2 KB of the body the endpoint answered this attempt with, so a handler that answers 200 and ignores a delivery can be seen doing so. It is whatever came back, which may echo what was sent. Null when no answer came back, and cleared when the message's content is discarded or the sent email is deleted. Withheld from a caller who cannot read message content, since an answer can echo what was sent.",
48553
+ "description": "The first 2 KB of the body the endpoint answered this attempt with, so a handler that answers 200 and ignores a delivery can be seen doing so. It is whatever came back, which may echo what was sent. Null when no answer came back, and cleared when the message's content is discarded or the sent email is deleted. Withheld from a caller who cannot read message content, since an answer can echo what was sent. Null for a redirect (3xx), whose body repeats its Location; otherwise the webhook URL's path and query are hidden wherever it repeats them.",
46690
48554
  "type": ["string", "null"]
46691
48555
  },
46692
48556
  "created_at": { "type": "string" },
@@ -46739,7 +48603,7 @@ const operationManifest = [
46739
48603
  "description": "The HTTP status the endpoint answered this attempt with: a webhook's own answer, or for a Function what its runtime answered, which is the platform's own error when the Function could not be run. Null when no answer came back (a network error, a timeout, an attempt refused or not sent) and on attempts recorded before this field."
46740
48604
  },
46741
48605
  "response_body": {
46742
- "description": "The first 2 KB of the body the endpoint answered this attempt with, so a handler that answers 200 and ignores a delivery can be seen doing so. It is whatever came back, which may echo what was sent. Null when no answer came back, and cleared when the message's content is discarded or the sent email is deleted. Withheld from a caller who cannot read message content, since an answer can echo what was sent.",
48606
+ "description": "The first 2 KB of the body the endpoint answered this attempt with, so a handler that answers 200 and ignores a delivery can be seen doing so. It is whatever came back, which may echo what was sent. Null when no answer came back, and cleared when the message's content is discarded or the sent email is deleted. Withheld from a caller who cannot read message content, since an answer can echo what was sent. Null for a redirect (3xx), whose body repeats its Location; otherwise the webhook URL's path and query are hidden wherever it repeats them.",
46743
48607
  "type": ["string", "null"]
46744
48608
  },
46745
48609
  "retry_scheduled": {
@@ -46896,7 +48760,10 @@ const operationManifest = [
46896
48760
  "type": "string",
46897
48761
  "format": "uuid"
46898
48762
  },
46899
- "endpoint_url": { "type": "string" },
48763
+ "endpoint_url": {
48764
+ "type": "string",
48765
+ "description": "Where it was sent: a webhook's scheme and host, followed by \"/... (path and query hidden)\" when its URL has more (GET /endpoints/{id} has the whole URL), function://<id> for a Function, or pull: for a pull subscription."
48766
+ },
46900
48767
  "status": {
46901
48768
  "type": "string",
46902
48769
  "enum": [
@@ -46949,7 +48816,7 @@ const operationManifest = [
46949
48816
  },
46950
48817
  "response_body": {
46951
48818
  "type": ["string", "null"],
46952
- "description": "The first 2 KB of the body the endpoint answered this attempt with, so a handler that answers 200 and ignores a delivery can be seen doing so. It is whatever came back, which may echo what was sent. Null when no answer came back, and cleared when the message's content is discarded or the sent email is deleted. Withheld from a caller who cannot read message content, since an answer can echo what was sent."
48819
+ "description": "The first 2 KB of the body the endpoint answered this attempt with, so a handler that answers 200 and ignores a delivery can be seen doing so. It is whatever came back, which may echo what was sent. Null when no answer came back, and cleared when the message's content is discarded or the sent email is deleted. Withheld from a caller who cannot read message content, since an answer can echo what was sent. Null for a redirect (3xx), whose body repeats its Location; otherwise the webhook URL's path and query are hidden wherever it repeats them."
46953
48820
  },
46954
48821
  "latest_attempt": {
46955
48822
  "type": "boolean",
@@ -47171,7 +49038,7 @@ function sanitizeForTerminal(value) {
47171
49038
  return out;
47172
49039
  }
47173
49040
  // src/oclif/answer-reports.ts
47174
- function isRecord$2(value) {
49041
+ function isRecord$3(value) {
47175
49042
  return value !== null && typeof value === "object" && !Array.isArray(value);
47176
49043
  }
47177
49044
  /** Text from an answer, made safe and one line for a report: no terminal controls. */
@@ -47180,10 +49047,10 @@ function oneLine(text) {
47180
49047
  }
47181
49048
  /** One line per warning a route write answers with (createRoute, updateRoute). */
47182
49049
  function routeWarningLines(data) {
47183
- const warnings = isRecord$2(data) ? data.warnings : void 0;
49050
+ const warnings = isRecord$3(data) ? data.warnings : void 0;
47184
49051
  if (!Array.isArray(warnings)) return [];
47185
49052
  return warnings.flatMap((warning) => {
47186
- if (!isRecord$2(warning)) return [];
49053
+ if (!isRecord$3(warning)) return [];
47187
49054
  const message = typeof warning.message === "string" ? oneLine(warning.message) : "";
47188
49055
  if (message) return [`Route warning: ${withCliCommands(message)}`];
47189
49056
  const code = typeof warning.code === "string" ? oneLine(warning.code) : "";
@@ -47211,11 +49078,11 @@ const DELIVERED_STATUSES = /* @__PURE__ */ new Set(["delivered", "header_confirm
47211
49078
  * and has no recorded answer says so.
47212
49079
  */
47213
49080
  function deliveryAttemptLines(data) {
47214
- const attempts = isRecord$2(data) ? data.attempts : void 0;
49081
+ const attempts = isRecord$3(data) ? data.attempts : void 0;
47215
49082
  if (!Array.isArray(attempts)) return [];
47216
49083
  const lines = [];
47217
49084
  for (const attempt of attempts) {
47218
- if (!isRecord$2(attempt)) continue;
49085
+ if (!isRecord$3(attempt)) continue;
47219
49086
  const text = (value) => typeof value === "string" && value ? oneLine(value) : "";
47220
49087
  const status = text(attempt.status);
47221
49088
  const parts = [
@@ -47309,7 +49176,7 @@ var CliOrgRefusedError = class extends Errors.CLIError {
47309
49176
  this.orgId = params.orgId;
47310
49177
  }
47311
49178
  };
47312
- function isRecord$1(value) {
49179
+ function isRecord$2(value) {
47313
49180
  return value !== null && typeof value === "object" && !Array.isArray(value);
47314
49181
  }
47315
49182
  function apiOrigin(apiBaseUrl) {
@@ -47336,7 +49203,7 @@ function isUuid(value) {
47336
49203
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
47337
49204
  }
47338
49205
  function isOrgAccessToken(value) {
47339
- return isRecord$1(value) && typeof value.access_token === "string" && value.access_token.length > 0 && value.token_type === "Bearer" && typeof value.expires_in === "number" && value.expires_in > 0 && typeof value.org_id === "string" && (value.org_name === null || typeof value.org_name === "string") && typeof value.oauth_grant_id === "string";
49206
+ return isRecord$2(value) && typeof value.access_token === "string" && value.access_token.length > 0 && value.token_type === "Bearer" && typeof value.expires_in === "number" && value.expires_in > 0 && typeof value.org_id === "string" && (value.org_name === null || typeof value.org_name === "string") && typeof value.oauth_grant_id === "string";
47340
49207
  }
47341
49208
  function orgTokenFromResponse(token, now = Date.now) {
47342
49209
  return {
@@ -47383,7 +49250,7 @@ async function postTokenEndpoint(params) {
47383
49250
  throw new Errors.CLIError(`Could not reach the Primitive OAuth token endpoint: ${detail}`, { exit: 1 });
47384
49251
  }
47385
49252
  const payload = await response.json().catch(() => null);
47386
- if (response.ok && isRecord$1(payload)) return {
49253
+ if (response.ok && isRecord$2(payload)) return {
47387
49254
  ok: true,
47388
49255
  payload
47389
49256
  };
@@ -47391,7 +49258,7 @@ async function postTokenEndpoint(params) {
47391
49258
  ok: true,
47392
49259
  payload: {}
47393
49260
  };
47394
- const record = isRecord$1(payload) ? payload : {};
49261
+ const record = isRecord$2(payload) ? payload : {};
47395
49262
  return {
47396
49263
  ok: false,
47397
49264
  status: response.status,
@@ -47594,12 +49461,12 @@ async function fetchCliOrgs(params) {
47594
49461
  }
47595
49462
  const payload = await response.json().catch(() => null);
47596
49463
  if (!response.ok) {
47597
- const error = isRecord$1(payload) && isRecord$1(payload.error) ? payload.error : {};
49464
+ const error = isRecord$2(payload) && isRecord$2(payload.error) ? payload.error : {};
47598
49465
  if (response.status === 404 && error.code === "feature_disabled") throw new Errors.CLIError("This Primitive server does not support switching organizations for CLI logins.", { exit: 1 });
47599
49466
  throw new Errors.CLIError(`Could not list organizations${typeof error.message === "string" ? `: ${error.message}` : ` (HTTP ${response.status}).`}`, { exit: 1 });
47600
49467
  }
47601
- const data = isRecord$1(payload) ? payload.data : null;
47602
- if (!isRecord$1(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
49468
+ const data = isRecord$2(payload) ? payload.data : null;
49469
+ if (!isRecord$2(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
47603
49470
  return data;
47604
49471
  }
47605
49472
  /** How long a command waits for another CLI process to finish with the saved login. */
@@ -48172,6 +50039,379 @@ async function withAttachmentWait(call, options) {
48172
50039
  }
48173
50040
  return result;
48174
50041
  }
50042
+ // src/oclif/email-rule-samples.ts
50043
+ function errorDetail(error) {
50044
+ return error instanceof Error ? error.message : String(error);
50045
+ }
50046
+ function usageError$1(message) {
50047
+ return new Errors.CLIError(message, { exit: 2 });
50048
+ }
50049
+ function isRecord$1(value) {
50050
+ return value !== null && typeof value === "object" && !Array.isArray(value);
50051
+ }
50052
+ /** A path as a shell word: quoted when it holds anything but plain characters. */
50053
+ function shellWord(value) {
50054
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
50055
+ }
50056
+ /** A JSON file a flag names, parsed, or an exit-1 error saying why not. */
50057
+ function readJsonFileFlag(path, flag) {
50058
+ let text;
50059
+ try {
50060
+ text = readFileSync(path, "utf8");
50061
+ } catch (error) {
50062
+ throw new Errors.CLIError(`Could not read ${flag} ${path}: ${errorDetail(error)}`, { exit: 1 });
50063
+ }
50064
+ try {
50065
+ return JSON.parse(text);
50066
+ } catch (error) {
50067
+ throw new Errors.CLIError(`${flag} ${path} is not valid JSON: ${errorDetail(error)}`, { exit: 1 });
50068
+ }
50069
+ }
50070
+ /** The longest label the API keeps on a sample. */
50071
+ const SAMPLE_LABEL_MAX = 200;
50072
+ /**
50073
+ * `text` cut to `max` UTF-16 units, as the API counts a label's length,
50074
+ * never inside a character.
50075
+ */
50076
+ function cutLabel(text, max) {
50077
+ const cut = text.slice(0, max);
50078
+ return (/[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut).trimEnd();
50079
+ }
50080
+ /**
50081
+ * Whether a look-alike is one the rule should match: its `expected` as the
50082
+ * API gives it. An answer without one (an older server) is must-not-match,
50083
+ * as every look-alike was.
50084
+ */
50085
+ function lookAlikeExpected(lookAlike) {
50086
+ return lookAlike.expected === "match" ? "match" : "no_match";
50087
+ }
50088
+ /**
50089
+ * A look-alike as a sample, keeping its `expected` (most must not match; a
50090
+ * near case the rule should take must match): the email as written,
50091
+ * labelled with its kind and why ("medical: ..."), or the why alone when
50092
+ * both do not fit a label.
50093
+ */
50094
+ function lookAlikeSample(lookAlike) {
50095
+ if (!isRecord$1(lookAlike) || !isRecord$1(lookAlike.email)) return null;
50096
+ const kind = typeof lookAlike.kind === "string" ? lookAlike.kind : "";
50097
+ const why = typeof lookAlike.why === "string" ? lookAlike.why.trim() : "";
50098
+ const both = kind && why ? `${kind}: ${why}` : kind || why;
50099
+ const label = both.length <= SAMPLE_LABEL_MAX ? both : why.length <= SAMPLE_LABEL_MAX ? why : `${cutLabel(why, 197)}...`;
50100
+ return {
50101
+ expected: lookAlikeExpected(lookAlike),
50102
+ ...label ? { label } : {},
50103
+ email: lookAlike.email
50104
+ };
50105
+ }
50106
+ /**
50107
+ * The look-alikes of a generate-look-alikes answer that can be saved (each
50108
+ * with an email), in the answer's order. The report numbers these and the
50109
+ * samples file holds these, so a number means the same email in both.
50110
+ */
50111
+ function lookAlikeEntries(answer) {
50112
+ const list = isRecord$1(answer) && Array.isArray(answer.look_alikes) ? answer.look_alikes : [];
50113
+ const entries = [];
50114
+ for (const item of list) {
50115
+ const sample = lookAlikeSample(item);
50116
+ if (sample && isRecord$1(item)) entries.push({
50117
+ lookAlike: item,
50118
+ sample
50119
+ });
50120
+ }
50121
+ return entries;
50122
+ }
50123
+ /** The look-alikes of a generate-look-alikes answer, as samples, in order. */
50124
+ function lookAlikeSamples(answer) {
50125
+ return lookAlikeEntries(answer).map((entry) => entry.sample);
50126
+ }
50127
+ /** The fields a saved made-up email takes, and the API's bounds on them. */
50128
+ const SAMPLE_EMAIL_KEYS = /* @__PURE__ */ new Set([
50129
+ "from",
50130
+ "to",
50131
+ "cc",
50132
+ "subject",
50133
+ "text",
50134
+ "attachment_types",
50135
+ "automated",
50136
+ "from_mailing_list",
50137
+ "spam_score"
50138
+ ]);
50139
+ const SAMPLE_TEXT_MAX = 4e3;
50140
+ const SAMPLE_SUBJECT_MAX = 998;
50141
+ const SAMPLE_LIST_MAX = 10;
50142
+ /**
50143
+ * Refuse, naming the sample and the field, a made-up email the API would
50144
+ * refuse to keep: the fields it takes and their bounds. The API checks the
50145
+ * rest (address forms, a NUL).
50146
+ */
50147
+ function checkSampleEmail(email, at) {
50148
+ const fail = (what) => {
50149
+ throw usageError$1(`${at}: email ${what}.`);
50150
+ };
50151
+ const unknown = Object.keys(email).find((key) => !SAMPLE_EMAIL_KEYS.has(key));
50152
+ if (unknown !== void 0) fail(`takes no "${unknown}" (it takes ${[...SAMPLE_EMAIL_KEYS].join(", ")})`);
50153
+ if (!isRecord$1(email.from) || typeof email.from.address !== "string") fail("needs from.address");
50154
+ const list = (key, min) => {
50155
+ const value = email[key];
50156
+ if (value === void 0 && min === 0) return;
50157
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string") || value.length < min || value.length > SAMPLE_LIST_MAX) fail(`${key} must be a list of ${min > 0 ? `${min} to ` : "at most "}${SAMPLE_LIST_MAX} strings`);
50158
+ };
50159
+ list("to", 1);
50160
+ list("cc", 0);
50161
+ list("attachment_types", 0);
50162
+ if (email.subject !== void 0 && (typeof email.subject !== "string" || email.subject.length > SAMPLE_SUBJECT_MAX)) fail(`subject must be a string of at most ${SAMPLE_SUBJECT_MAX} characters`);
50163
+ if (typeof email.text !== "string" || email.text.length === 0 || email.text.length > SAMPLE_TEXT_MAX) fail(`text must be 1 to ${SAMPLE_TEXT_MAX} characters`);
50164
+ for (const key of ["automated", "from_mailing_list"]) if (email[key] !== void 0 && typeof email[key] !== "boolean") fail(`${key} must be true or false`);
50165
+ if (email.spam_score !== void 0 && (typeof email.spam_score !== "number" || !Number.isFinite(email.spam_score))) fail("spam_score must be a number");
50166
+ }
50167
+ /** The keys a sample may carry: a saved one's id and time are taken and ignored. */
50168
+ const SAMPLE_KEYS = /* @__PURE__ */ new Set([
50169
+ "expected",
50170
+ "label",
50171
+ "email",
50172
+ "id",
50173
+ "created_at"
50174
+ ]);
50175
+ /**
50176
+ * The samples a file holds: a JSON array of samples, as --samples-file
50177
+ * writes them, or a generate-look-alikes answer saved as the command printed
50178
+ * it (with or without --json), whose look-alikes become samples, each
50179
+ * keeping whether it must match, numbered as its report numbered them. Only a literal [] holds no
50180
+ * samples; an answer with none is refused, so it never empties a rule's.
50181
+ */
50182
+ function samplesFromJson(value, source) {
50183
+ const data = isRecord$1(value) && isRecord$1(value.data) ? value.data : value;
50184
+ if (isRecord$1(data) && Array.isArray(data.look_alikes)) {
50185
+ const samples = lookAlikeSamples(data);
50186
+ if (samples.length === 0) throw usageError$1(`${source} is a generate-look-alikes answer with no look-alikes in it.`);
50187
+ samples.forEach((sample, index) => {
50188
+ checkSampleEmail(sample.email, `${source}, look-alike ${index + 1}`);
50189
+ });
50190
+ return samples;
50191
+ }
50192
+ if (!Array.isArray(data)) throw usageError$1(`${source} must hold a JSON array of samples, such as [{"expected": "no_match", "label": "a personal bill", "email": {...}}], or a generate-look-alikes answer.`);
50193
+ return data.map((item, index) => {
50194
+ const at = `${source}, sample ${index + 1}`;
50195
+ if (!isRecord$1(item)) throw usageError$1(`${at} is not an object.`);
50196
+ const unknown = Object.keys(item).find((key) => !SAMPLE_KEYS.has(key));
50197
+ if (unknown !== void 0) throw usageError$1(`${at}: "${unknown}" is not a sample field; a sample takes expected, label and email.`);
50198
+ if (item.expected !== "match" && item.expected !== "no_match") throw usageError$1(`${at}: expected must be "match" or "no_match".`);
50199
+ if (!isRecord$1(item.email)) throw usageError$1(`${at}: email must be an object, as test-email-rule-on-sample takes it.`);
50200
+ checkSampleEmail(item.email, at);
50201
+ const label = item.label;
50202
+ if (label !== void 0 && label !== null) {
50203
+ if (typeof label !== "string") throw usageError$1(`${at}: label must be a string.`);
50204
+ if (label.trim() === "") throw usageError$1(`${at}: label is blank; leave it out or say what the email stands for.`);
50205
+ if (label.trim().length > SAMPLE_LABEL_MAX) throw usageError$1(`${at}: label is longer than ${SAMPLE_LABEL_MAX} characters.`);
50206
+ }
50207
+ return {
50208
+ expected: item.expected,
50209
+ ...label !== void 0 ? { label } : {},
50210
+ email: item.email
50211
+ };
50212
+ });
50213
+ }
50214
+ /**
50215
+ * The numbers --pick names, from 1, in the order given and without repeats,
50216
+ * or a usage error: "2,4" or "2 4". `count` is how many there are to pick
50217
+ * from; a number past it is refused, rather than skipped.
50218
+ */
50219
+ function pickNumbers(pick, count, source) {
50220
+ const parts = pick.split(/[\s,]+/).map((part) => part.trim()).filter((part) => part !== "");
50221
+ if (parts.length === 0) throw usageError$1("--pick needs the numbers of the samples to take, such as --pick 2,4.");
50222
+ const chosen = [];
50223
+ for (const part of parts) {
50224
+ if (!/^\d+$/.test(part)) throw usageError$1(`--pick ${pick}: "${part}" is not a sample number.`);
50225
+ const n = Number(part);
50226
+ if (n < 1 || n > count) throw usageError$1(count === 0 ? `--pick ${pick}: ${source} holds no samples.` : `--pick ${pick}: ${n} is not a sample number; ${source} holds ${count === 1 ? "sample 1" : `samples 1 to ${count}`}.`);
50227
+ if (!chosen.includes(n)) chosen.push(n);
50228
+ }
50229
+ return chosen;
50230
+ }
50231
+ /** The samples --pick chooses, by their numbers from 1. */
50232
+ function pickSamples(samples, pick, source) {
50233
+ return pickNumbers(pick, samples.length, source).map((n) => samples[n - 1]);
50234
+ }
50235
+ /**
50236
+ * The number a person knows a sample a command sent by: its number in the
50237
+ * samples file when --pick took it, else its place from 1 in what was sent.
50238
+ * `index` is its place in what was sent, from 0, as the API gives it.
50239
+ */
50240
+ function sentSampleNumber(index, flags) {
50241
+ const pick = flags?.[PICK_FLAG];
50242
+ if (typeof pick !== "string") return index + 1;
50243
+ try {
50244
+ return pickNumbers(pick, Number.MAX_SAFE_INTEGER, "")[index] ?? index + 1;
50245
+ } catch {
50246
+ return index + 1;
50247
+ }
50248
+ }
50249
+ /** The samples file flags: replace (create and update), add (update), and test. */
50250
+ const SAMPLES_FILE_FLAG = "expectation-samples-file";
50251
+ const ADD_SAMPLES_FILE_FLAG = "add-expectation-samples-file";
50252
+ const EXPECT_SAMPLES_FILE_FLAG = "expect-samples-file";
50253
+ const PICK_FLAG = "pick";
50254
+ /** generate-look-alikes' flag for the file it writes them to. */
50255
+ const LOOK_ALIKES_SAMPLES_FILE_FLAG = "samples-file";
50256
+ /** The samples a samples file flag names, only those --pick chooses if given. */
50257
+ function readSamplesFile(path, flag, pick) {
50258
+ const source = `--${flag} ${path}`;
50259
+ const all = samplesFromJson(readJsonFileFlag(path, `--${flag}`), source);
50260
+ const samples = typeof pick === "string" ? pickSamples(all, pick, source) : all;
50261
+ if (samples.length > 20) throw usageError$1(`${source} holds ${samples.length} samples, and a rule or a test takes at most 20; take some with --pick.`);
50262
+ return samples;
50263
+ }
50264
+ /** The one of `names` given as a file path, or null; --pick without one is refused. */
50265
+ function givenSamplesFlag(flags, names) {
50266
+ const given = names.find((name) => typeof flags[name] === "string") ?? null;
50267
+ if (given === null && typeof flags["pick"] === "string") throw usageError$1(`--pick chooses from a samples file: pass it with ${names.map((name) => `--${name}`).join(" or ")}.`);
50268
+ return given;
50269
+ }
50270
+ /**
50271
+ * The body fields a samples file sets on create-email-rule and
50272
+ * update-email-rule: expectation_samples (on update it replaces them) or, on
50273
+ * update, add_expectation_samples. With --pick, only the samples it names.
50274
+ */
50275
+ function samplesBodyFields(canAdd) {
50276
+ const names = canAdd ? [SAMPLES_FILE_FLAG, ADD_SAMPLES_FILE_FLAG] : [SAMPLES_FILE_FLAG];
50277
+ return (flags) => {
50278
+ const flag = givenSamplesFlag(flags, names);
50279
+ if (flag === null) return [];
50280
+ const path = flags[flag];
50281
+ const samples = readSamplesFile(path, flag, flags[PICK_FLAG]);
50282
+ if (samples.length === 0 && !(canAdd && flag === "expectation-samples-file")) throw usageError$1(`--${flag} ${path} holds no samples.`);
50283
+ return [{
50284
+ flag: `--${flag}`,
50285
+ property: flag === "add-expectation-samples-file" ? "add_expectation_samples" : "expectation_samples",
50286
+ value: samples
50287
+ }];
50288
+ };
50289
+ }
50290
+ /**
50291
+ * The samples test-email-rule expects, from --expect-samples-file (only
50292
+ * those --pick chooses if given), or null without the flag. They go in
50293
+ * expect.samples.
50294
+ */
50295
+ function expectSamples(flags) {
50296
+ const flag = givenSamplesFlag(flags, [EXPECT_SAMPLES_FILE_FLAG]);
50297
+ if (flag === null) return null;
50298
+ const path = flags[flag];
50299
+ const samples = readSamplesFile(path, flag, flags[PICK_FLAG]);
50300
+ if (samples.length === 0) throw usageError$1(`--${flag} ${path} holds no samples.`);
50301
+ return samples;
50302
+ }
50303
+ /** --pick's help, naming the flags it chooses from. */
50304
+ function pickFlagHelp(flags) {
50305
+ return `Take only these samples from ${flags.map((flag) => `--${flag}`).join(" or ")}, by their numbers from 1 as generate-look-alikes printed them: --pick 2,4. A number the file does not hold is refused.`;
50306
+ }
50307
+ /**
50308
+ * What the rule decided for a look-alike, and what its status makes of it,
50309
+ * against what it should do: leave it (`no_match`), or take it (`match`).
50310
+ */
50311
+ function decidedLine(outcome, status, expected) {
50312
+ if (outcome === "not_decided") return "The rule did not decide it: it was not reached in time, or deciding it failed.";
50313
+ if (outcome === "matched") return status === "undecided" ? "The rule matched it, but it reads a spam score, which a look-alike does not carry, so this one is undecided." : expected === "match" ? "The rule matched it, as it should." : "The rule matched it, so mail like it would be shared.";
50314
+ const decided = outcome === "near_miss" ? "The rule nearly matched it, but did not" : "The rule did not match it";
50315
+ if (status === "undecided") return `${decided}, on something it could not decide (its classification failed, a fact or a question's answer is missing, or it reads a spam score, which a look-alike does not carry), so this one is undecided.`;
50316
+ if (expected === "no_match") return `${decided}.`;
50317
+ return `${decided}, ${outcome === "near_miss" ? "though" : "but"} it should: mail like it would be missed.`;
50318
+ }
50319
+ /**
50320
+ * The stderr report of generate-look-alikes: each look-alike numbered as the
50321
+ * samples file numbers it, with its status, kind, subject, sender, why the
50322
+ * rule should (or should not) match it and what the rule decided; the
50323
+ * counts; the answer's note; and how to save the ones you choose. With
50324
+ * --samples-file it writes every look-alike there as a sample keeping its
50325
+ * expected, in that order.
50326
+ */
50327
+ function lookAlikesReport(data, request) {
50328
+ const file = request.flags?.[LOOK_ALIKES_SAMPLES_FILE_FLAG];
50329
+ if (!isRecord$1(data)) {
50330
+ if (typeof file !== "string") return { lines: [] };
50331
+ const message = `Did not write --${LOOK_ALIKES_SAMPLES_FILE_FLAG} ${file}: the answer held no look-alikes to write.`;
50332
+ return {
50333
+ lines: [message],
50334
+ exitCode: 1,
50335
+ error: {
50336
+ code: "samples_file_not_written",
50337
+ message
50338
+ }
50339
+ };
50340
+ }
50341
+ const entries = lookAlikeEntries(data);
50342
+ const lines = [];
50343
+ entries.forEach(({ lookAlike }, index) => {
50344
+ const email = lookAlike.email;
50345
+ const from = isRecord$1(email.from) && typeof email.from.address === "string" ? email.from.address : "an unknown sender";
50346
+ const subject = typeof email.subject === "string" && email.subject ? `"${email.subject}"` : "(no subject)";
50347
+ const status = typeof lookAlike.status === "string" ? lookAlike.status : "undecided";
50348
+ const kind = typeof lookAlike.kind === "string" ? lookAlike.kind : "other";
50349
+ const outcome = typeof lookAlike.outcome === "string" ? lookAlike.outcome : "not_decided";
50350
+ const expected = lookAlikeExpected(lookAlike);
50351
+ lines.push(`${index + 1}. ${status} ${kind} ${subject} from ${from}`);
50352
+ if (typeof lookAlike.why === "string" && lookAlike.why.trim()) lines.push(` Why it should ${expected === "match" ? "" : "not "}match: ${lookAlike.why.trim()}`);
50353
+ lines.push(` ${decidedLine(outcome, status, expected)}`);
50354
+ });
50355
+ const counts = isRecord$1(data.counts) ? data.counts : {};
50356
+ const count = (key) => typeof counts[key] === "number" ? counts[key] : 0;
50357
+ const dropped = count("dropped");
50358
+ lines.push(`Counts: ${count("passed")} passed, ${count("failed")} failed, ${count("undecided")} undecided${dropped > 0 ? `; ${dropped} asked for did not come back` : ""}.`);
50359
+ if (typeof data.note === "string" && data.note.trim()) lines.push(data.note.trim());
50360
+ const numbered = (status) => entries.map(({ lookAlike }, index) => lookAlike.status === status ? index + 1 : 0).filter((n) => n > 0);
50361
+ const wanted = (n) => entries[n - 1]?.sample.expected === "match";
50362
+ const failed = numbered("failed");
50363
+ const missed = failed.filter(wanted);
50364
+ const matched = failed.filter((n) => !wanted(n));
50365
+ const undecided = numbered("undecided");
50366
+ const chosen = failed.length > 0 ? failed : undecided.length > 0 ? numbered("passed") : [];
50367
+ const pick = chosen.length > 0 ? ` --pick ${chosen.join(",")}` : "";
50368
+ const body = isRecord$1(request.body) ? request.body : {};
50369
+ const ruleId = typeof body.rule_id === "string" ? body.rule_id : null;
50370
+ const requestId = typeof body.request_id === "string" ? body.request_id : null;
50371
+ const target = typeof file === "string" ? shellWord(file) : "<file>";
50372
+ const save = ruleId ? `\`primitive email-rules update-email-rule ${ruleId} --${ADD_SAMPLES_FILE_FLAG} ${target}${pick}\`` : requestId ? `\`primitive email-rules accept-email-rule-request ${requestId} --${SAMPLES_FILE_FLAG} ${target}${pick}\` when the member the request asks accepts it` : `\`primitive email-rules create-email-rule ... --${SAMPLES_FILE_FLAG} ${target}${pick}\` when you save the rule`;
50373
+ const one = (count, single, many) => count === 1 ? single : many;
50374
+ const noneDecided = entries.length > 0 && undecided.length === entries.length ? `None of them was decided (each says why above), so saved as they are they may stay undecided; if the rule reads a spam score, give each email a spam_score${typeof file === "string" ? " in the file" : ""} first.` : "";
50375
+ const keepMatched = `${one(matched.length, "the one it matched as an email", "the ones it matched as emails")} it must not match`;
50376
+ const keepMissed = `${one(missed.length, "the one it missed as an email", "the ones it missed as emails")} it must match`;
50377
+ const which = failed.length > 0 ? `Keep ${matched.length > 0 && missed.length > 0 ? `${keepMatched}, and ${keepMissed}` : matched.length > 0 ? keepMatched : keepMissed}` : chosen.length > 0 ? chosen.some(wanted) ? `Keep ${one(chosen.length, "the one", "the ones")} it decided as expected (each keeps whether it must match; an undecided one may stay undecided as a sample)` : `Keep ${one(chosen.length, "the one it did not match as an email", "the ones it did not match as emails")} it must not match (an undecided one may stay undecided as a sample)` : entries.some((_, index) => wanted(index + 1)) ? `Keep ${one(entries.length, "it as an email", "them as emails")} the rule must match or not, as each says` : `Keep ${one(entries.length, "it as an email", "them as emails")} the rule must not match`;
50378
+ const test = failed.length > 0 ? ` Test an edited rule on ${one(failed.length, "it", "them")} with \`primitive email-rules test-email-rule ${ruleId ? `--rule-id ${ruleId}` : "--body-file <body.json>"} --${EXPECT_SAMPLES_FILE_FLAG} ${target}${pick}\`.` : "";
50379
+ if (typeof file === "string") {
50380
+ const samples = entries.map((entry) => entry.sample);
50381
+ if (samples.length === 0) {
50382
+ const message = `Did not write --${LOOK_ALIKES_SAMPLES_FILE_FLAG} ${file}: no look-alikes came back, so it was left as it was.`;
50383
+ return {
50384
+ lines: [...lines, message],
50385
+ exitCode: 1,
50386
+ error: {
50387
+ code: "samples_file_not_written",
50388
+ message
50389
+ }
50390
+ };
50391
+ }
50392
+ try {
50393
+ writeFileSync(file, `${JSON.stringify(samples, null, 2)}\n`);
50394
+ } catch (error) {
50395
+ const message = `Could not write --${LOOK_ALIKES_SAMPLES_FILE_FLAG} ${file}: ${errorDetail(error)}`;
50396
+ return {
50397
+ lines: [
50398
+ ...lines,
50399
+ message,
50400
+ "The JSON printed above holds these same look-alikes: save it to a file and give that file instead. Running generate-look-alikes again writes new ones."
50401
+ ],
50402
+ exitCode: 1,
50403
+ error: {
50404
+ code: "samples_file_not_written",
50405
+ message
50406
+ }
50407
+ };
50408
+ }
50409
+ const wrote = `Wrote ${one(samples.length, "the look-alike", `the ${samples.length} look-alikes`)} to ${file}, numbered as above.`;
50410
+ lines.push(noneDecided ? `${wrote} ${noneDecided}` : `${wrote} ${which}, with ${save}; --pick takes any of the numbers.${test}`);
50411
+ } else if (noneDecided) lines.push(noneDecided);
50412
+ else if (entries.length > 0) lines.push(`${which}: save the JSON printed above to a file and give it to ${save}; --pick takes any of the numbers above.${test}`);
50413
+ return { lines };
50414
+ }
48175
50415
  // src/oclif/email-rule-authoring.ts
48176
50416
  const EMAIL_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
48177
50417
  /** Whether `id` has the shape of an email id (a UUID). */
@@ -48184,9 +50424,6 @@ function usageError(message) {
48184
50424
  function isRecord(value) {
48185
50425
  return value !== null && typeof value === "object" && !Array.isArray(value);
48186
50426
  }
48187
- function errorDetail(error) {
48188
- return error instanceof Error ? error.message : String(error);
48189
- }
48190
50427
  /** `count` and the word for it: "1 expectation", "2 expectations". */
48191
50428
  function plural$1(count, one, many) {
48192
50429
  return `${count} ${count === 1 ? one : many}`;
@@ -48227,7 +50464,7 @@ function checkExpectations(value, label) {
48227
50464
  if (!isRecord(value)) throw usageError(`${label} must hold a JSON object such as {"match": ["<email-id>"], "no_match": ["<email-id>"]}.`);
48228
50465
  const expectations = {};
48229
50466
  for (const [key, list] of Object.entries(value)) {
48230
- if (key !== "match" && key !== "no_match") throw usageError(`${label} takes only "match" and "no_match"; "${key}" is not one of them.`);
50467
+ if (key !== "match" && key !== "no_match") throw usageError(key === "samples" ? `${label} takes emails by id only; give made-up emails with ${label.startsWith("--expectations-file") ? "--expectation-samples-file" : "--expect-samples-file"}.` : `${label} takes only "match" and "no_match"; "${key}" is not one of them.`);
48231
50468
  if (!Array.isArray(list) || list.some((id) => typeof id !== "string")) throw usageError(`${label}: "${key}" must be a list of email ids.`);
48232
50469
  const ids = [];
48233
50470
  const seen = /* @__PURE__ */ new Set();
@@ -48243,33 +50480,19 @@ function checkExpectations(value, label) {
48243
50480
  assertDisjoint(expectations, label);
48244
50481
  return expectations;
48245
50482
  }
48246
- /** Read and parse a JSON file a flag names, failing like --body-file does. */
48247
- function readJsonFileFlag(path, flag) {
48248
- let text;
48249
- try {
48250
- text = readFileSync(path, "utf8");
48251
- } catch (error) {
48252
- throw new Errors.CLIError(`Could not read ${flag} ${path}: ${errorDetail(error)}`, { exit: 1 });
48253
- }
48254
- try {
48255
- return JSON.parse(text);
48256
- } catch (error) {
48257
- throw new Errors.CLIError(`${flag} ${path} is not valid JSON: ${errorDetail(error)}`, { exit: 1 });
48258
- }
48259
- }
48260
- /** `expect` for test-email-rule, from --expect-file or the id flags. */
50483
+ /**
50484
+ * `expect` for test-email-rule, from --expect-file or the id flags, with the
50485
+ * made-up emails --expect-samples-file holds in expect.samples.
50486
+ */
48261
50487
  function expectBodyFields(flags) {
50488
+ const samples = expectSamples(flags);
48262
50489
  const file = flags["expect-file"];
48263
- if (typeof file === "string") {
48264
- const label = `--expect-file ${file}`;
48265
- return [{
48266
- flag: "--expect-file",
48267
- property: "expect",
48268
- value: checkExpectations(readJsonFileFlag(file, "--expect-file"), label)
48269
- }];
48270
- }
48271
50490
  const expect = {};
48272
50491
  const given = [];
50492
+ if (typeof file === "string") {
50493
+ Object.assign(expect, checkExpectations(readJsonFileFlag(file, "--expect-file"), `--expect-file ${file}`));
50494
+ given.push("--expect-file");
50495
+ }
48273
50496
  const match = flags["expect-match"];
48274
50497
  if (Array.isArray(match)) {
48275
50498
  expect.match = parseEmailIdList(match, "--expect-match");
@@ -48280,8 +50503,12 @@ function expectBodyFields(flags) {
48280
50503
  expect.no_match = parseEmailIdList(noMatch, "--expect-no-match");
48281
50504
  given.push("--expect-no-match");
48282
50505
  }
48283
- if (given.length === 0) return [];
48284
50506
  assertDisjoint(expect, given.join(" and "));
50507
+ if (samples) {
50508
+ expect.samples = samples;
50509
+ given.push(`--${EXPECT_SAMPLES_FILE_FLAG}`);
50510
+ }
50511
+ if (given.length === 0) return [];
48285
50512
  return [{
48286
50513
  flag: given.join(", "),
48287
50514
  property: "expect",
@@ -48418,18 +50645,72 @@ function expectationLine(result, results) {
48418
50645
  const note = result.status === "undecided" ? undecidedNote(undecidedReason(result, results)) : void 0;
48419
50646
  return ` ${result.status}: ${result.email_id} expected ${result.expected}, got ${result.outcome}${note ? ` (${note})` : ""}`;
48420
50647
  }
48421
- function expectationsLines(expectations, testResults) {
50648
+ /** The made-up emails a test's expectations judged, in order. */
50649
+ function sampleJudgments(expectations) {
50650
+ return (isRecord(expectations) && Array.isArray(expectations.samples) ? expectations.samples : []).filter(isRecord).map((item, position) => ({
50651
+ index: typeof item.index === "number" ? item.index : position,
50652
+ label: typeof item.label === "string" ? item.label : null,
50653
+ expected: String(item.expected ?? ""),
50654
+ outcome: String(item.outcome ?? "not_decided"),
50655
+ status: String(item.status ?? "undecided"),
50656
+ question_answers: Array.isArray(item.question_answers) ? item.question_answers.filter(isRecord) : []
50657
+ }));
50658
+ }
50659
+ function sampleNamer(request) {
50660
+ const sent = isRecord(request?.body) && isRecord(request.body.expect) && Array.isArray(request.body.expect.samples);
50661
+ const fromFile = sent && typeof request?.flags?.["expect-samples-file"] === "string";
50662
+ return ({ index, label }) => {
50663
+ const n = fromFile ? sentSampleNumber(index, request?.flags) : index + 1;
50664
+ const name = sent ? `sample ${n}` : `saved sample ${n}`;
50665
+ return label ? `${name} ("${label}")` : name;
50666
+ };
50667
+ }
50668
+ /**
50669
+ * Why a made-up email is undecided, and whether a later test can decide it:
50670
+ * yes when it was not reached or a question went unanswered; maybe not
50671
+ * otherwise, since a spam score it does not carry, a fact its
50672
+ * classification left out or a failed classification look the same. A
50673
+ * spam score left unset makes one undecided even with a question
50674
+ * unanswered, which then reads as decidable later: the answer cannot tell.
50675
+ */
50676
+ function sampleUndecidedReason(sample) {
50677
+ if (sample.outcome === "not_decided") return {
50678
+ why: "it was not reached in time, or deciding it failed",
50679
+ resolvable: true
50680
+ };
50681
+ const answers = sample.question_answers;
50682
+ if (answers.some((answer) => answer.answer === null) && !answers.some((answer) => answer.answer === false)) return {
50683
+ why: "a question went unanswered",
50684
+ resolvable: true
50685
+ };
50686
+ return {
50687
+ why: "it reads a spam score the sample does not carry, a fact its classification left out, or its classification failed",
50688
+ resolvable: false,
50689
+ may: true
50690
+ };
50691
+ }
50692
+ function sampleLines(sample, name) {
50693
+ const note = sample.status === "undecided" ? ` (${undecidedNote(sampleUndecidedReason(sample))})` : "";
50694
+ const lines = [` ${sample.status}: ${name(sample)} expected ${sample.expected}, got ${sample.outcome}${note}`];
50695
+ if (sample.question_answers.length > 0) lines.push(` questions: ${sample.question_answers.map(answerText).join("; ")}`);
50696
+ return lines;
50697
+ }
50698
+ function expectationsLines(expectations, testResults, name = sampleNamer()) {
48422
50699
  if (!expectations) return [];
48423
50700
  const results = Array.isArray(expectations.results) ? expectations.results : [];
48424
50701
  const passed = expectations.counts?.passed ?? 0;
48425
50702
  const failed = expectations.counts?.failed ?? 0;
48426
50703
  const undecided = expectations.counts?.undecided ?? 0;
48427
50704
  if (expectations.passed && failed === 0 && undecided === 0) return [`Expectations: all ${passed} passed.`];
48428
- return [`Expectations: ${[
48429
- failed > 0 ? `${failed} failed` : "",
48430
- undecided > 0 ? `${undecided} undecided` : "",
48431
- `${passed} passed`
48432
- ].filter((part) => part !== "").join(", ")}.`, ...results.filter((result) => result.status !== "passed").map((result) => expectationLine(result, testResults))];
50705
+ return [
50706
+ `Expectations: ${[
50707
+ failed > 0 ? `${failed} failed` : "",
50708
+ undecided > 0 ? `${undecided} undecided` : "",
50709
+ `${passed} passed`
50710
+ ].filter((part) => part !== "").join(", ")}.`,
50711
+ ...results.filter((result) => result.status !== "passed").map((result) => expectationLine(result, testResults)),
50712
+ ...sampleJudgments(expectations).filter((sample) => sample.status !== "passed").flatMap((sample) => sampleLines(sample, name))
50713
+ ];
48433
50714
  }
48434
50715
  const NOT_MATCHED_ORDER = {
48435
50716
  near_miss: 1,
@@ -48491,14 +50772,24 @@ function notMatchedLines(results, expectations) {
48491
50772
  * test can decide it;
48492
50773
  * - else nothing: the exit code stays 0.
48493
50774
  */
48494
- function expectationsOutcome(expectations, testResults = []) {
50775
+ function expectationsOutcome(expectations, testResults = [], name = sampleNamer()) {
48495
50776
  if (!expectations) return {};
48496
50777
  const results = Array.isArray(expectations.results) ? expectations.results : [];
48497
- const failed = results.filter((result) => result.status === "failed").map((result) => result.email_id);
48498
- const undecided = results.filter((result) => result.status === "undecided").map((result) => ({
50778
+ const samples = sampleJudgments(expectations);
50779
+ const failed = [...results.filter((result) => result.status === "failed").map((result) => result.email_id), ...samples.filter((sample) => sample.status === "failed").map((sample) => name({
50780
+ index: sample.index,
50781
+ label: null
50782
+ }))];
50783
+ const undecided = [...results.filter((result) => result.status === "undecided").map((result) => ({
48499
50784
  id: result.email_id,
48500
50785
  reason: undecidedReason(result, testResults)
48501
- }));
50786
+ })), ...samples.filter((sample) => sample.status === "undecided").map((sample) => ({
50787
+ id: name({
50788
+ index: sample.index,
50789
+ label: null
50790
+ }),
50791
+ reason: sampleUndecidedReason(sample)
50792
+ }))];
48502
50793
  const unresolvable = undecided.filter(({ reason }) => !reason.resolvable);
48503
50794
  const failedCount = Math.max(expectations.counts?.failed ?? 0, failed.length);
48504
50795
  const undecidedCount = Math.max(expectations.counts?.undecided ?? 0, undecided.length);
@@ -48514,7 +50805,7 @@ function expectationsOutcome(expectations, testResults = []) {
48514
50805
  exitCode: 1,
48515
50806
  error: {
48516
50807
  code: "expectations_unresolvable",
48517
- message: `${plural$1(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again will not decide ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
50808
+ message: `${plural$1(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again ${unresolvable.every(({ reason }) => reason.may) ? "may not decide" : "will not decide"} ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
48518
50809
  }
48519
50810
  };
48520
50811
  if (undecidedCount > 0) return {
@@ -48527,8 +50818,9 @@ function expectationsOutcome(expectations, testResults = []) {
48527
50818
  return {};
48528
50819
  }
48529
50820
  /** The stderr report and exit code of a test-email-rule answer. */
48530
- function testEmailRuleReport(data) {
50821
+ function testEmailRuleReport(data, request) {
48531
50822
  if (!isRecord(data)) return { lines: [] };
50823
+ const name = sampleNamer(request);
48532
50824
  const result = data;
48533
50825
  const results = Array.isArray(result.results) ? result.results : [];
48534
50826
  const summary = isRecord(result.summary) ? result.summary : void 0;
@@ -48538,23 +50830,56 @@ function testEmailRuleReport(data) {
48538
50830
  ...scopeWarningLines(result.scope_warnings),
48539
50831
  ...conditionLines(summary),
48540
50832
  ...questionLines(summary, results),
48541
- ...expectationsLines(result.expectations, results),
50833
+ ...expectationsLines(result.expectations, results, name),
48542
50834
  ...notMatchedLines(results, result.expectations)
48543
50835
  ],
48544
- ...expectationsOutcome(result.expectations, results)
50836
+ ...expectationsOutcome(result.expectations, results, name)
48545
50837
  };
48546
50838
  }
48547
- function expectationsCheckLine(check, ruleId) {
50839
+ /** A list of numbers from 0, as the API gives sample indexes. */
50840
+ function indexList(value) {
50841
+ return Array.isArray(value) ? value.filter((n) => typeof n === "number") : [];
50842
+ }
50843
+ /**
50844
+ * How a rule write names a made-up email its check gives by index, from 0 in
50845
+ * the set judged: those this write sends in place of the rule's
50846
+ * (expectation_samples), or the rule's saved ones oldest first and then
50847
+ * those it adds (add_expectation_samples). One this write sent is "sample
50848
+ * N", its number in the samples file --pick took it from (or its place in
50849
+ * what was sent); any other is "saved sample N", its place among the rule's.
50850
+ * With its label when known. `saved` is the answer's expectation_samples,
50851
+ * every one the rule keeps after the write, when it has them.
50852
+ */
50853
+ function writtenSampleNamer(body, flags, saved) {
50854
+ const sent = isRecord(body) && Array.isArray(body.expectation_samples) ? body.expectation_samples : null;
50855
+ const added = isRecord(body) && Array.isArray(body.add_expectation_samples) ? body.add_expectation_samples : null;
50856
+ const kept = Array.isArray(saved) ? saved : null;
50857
+ const labelled = (name, sample) => isRecord(sample) && typeof sample.label === "string" && sample.label ? `${name} ("${sample.label}")` : name;
50858
+ return (index) => {
50859
+ if (sent) return labelled(`sample ${sentSampleNumber(index, flags)}`, sent[index]);
50860
+ if (added) {
50861
+ const start = kept ? kept.length - added.length : null;
50862
+ if (start === null) return `made-up email ${index + 1}`;
50863
+ if (index >= start) return labelled(`sample ${sentSampleNumber(index - start, flags)}`, added[index - start]);
50864
+ }
50865
+ return labelled(`saved sample ${index + 1}`, kept?.[index]);
50866
+ };
50867
+ }
50868
+ function expectationsCheckLine(check, ruleId, name = (index) => `saved sample ${index + 1}`) {
48548
50869
  if (!isRecord(check)) return void 0;
48549
50870
  const on = typeof check.version === "number" ? `version ${check.version}` : "the current version";
48550
50871
  const failed = Array.isArray(check.failed) ? check.failed : [];
48551
50872
  const undecided = Array.isArray(check.undecided) ? check.undecided : [];
48552
- if (check.passed && failed.length === 0 && undecided.length === 0) return `Expectations on ${on}: all passed.`;
50873
+ const failedSamples = indexList(check.failed_samples);
50874
+ const undecidedSamples = indexList(check.undecided_samples);
50875
+ if (check.passed && failed.length === 0 && undecided.length === 0 && failedSamples.length === 0 && undecidedSamples.length === 0) return `Expectations on ${on}: all passed.`;
48553
50876
  const parts = [];
48554
50877
  if (failed.length > 0) parts.push(`${failed.length} failed (${failed.join(", ")})`);
48555
- else if (undecided.length > 0) parts.push("none failed");
50878
+ if (failedSamples.length > 0) parts.push(`${plural$1(failedSamples.length, "made-up email", "made-up emails")} failed (${failedSamples.map(name).join(", ")})`);
50879
+ if (failed.length === 0 && failedSamples.length === 0 && (undecided.length > 0 || undecidedSamples.length > 0)) parts.push("none failed");
48556
50880
  if (undecided.length > 0) parts.push(`${undecided.length} undecided (${undecided.join(", ")})`);
48557
- const rest = undecided.length > 0 ? ` To decide the rest, run \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.` : "";
50881
+ if (undecidedSamples.length > 0) parts.push(`${plural$1(undecidedSamples.length, "made-up email", "made-up emails")} not judged by this write (${undecidedSamples.map(name).join(", ")})`);
50882
+ const rest = undecided.length > 0 || undecidedSamples.length > 0 ? ` To decide the rest, run \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.` : "";
48558
50883
  const note = typeof check.note === "string" && check.note.trim() ? ` ${check.note.trim()}` : "";
48559
50884
  return `Expectations on ${on}: ${parts.length > 0 ? parts.join(", ") : "not passed"}.${rest}${note}`;
48560
50885
  }
@@ -48568,7 +50893,7 @@ const FAIL_ON_EXPECTATIONS_FLAG = "fail-on-expectations";
48568
50893
  * `passed` as false or as null when nothing failed and something is
48569
50894
  * undecided; any other check that did not pass exits 1.
48570
50895
  */
48571
- function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "") {
50896
+ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "", name = (index) => `saved sample ${index + 1}`) {
48572
50897
  const test = `\`primitive email-rules test-email-rule --rule-id ${ruleId}\``;
48573
50898
  if (!isRecord(check)) {
48574
50899
  if (!sentExpectations) return {};
@@ -48580,8 +50905,9 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "")
48580
50905
  }
48581
50906
  };
48582
50907
  }
48583
- const failed = Array.isArray(check.failed) ? check.failed : [];
48584
- const undecided = Array.isArray(check.undecided) ? check.undecided : [];
50908
+ const failed = [...Array.isArray(check.failed) ? check.failed : [], ...indexList(check.failed_samples).map(name)];
50909
+ const undecidedSamples = indexList(check.undecided_samples);
50910
+ const undecided = [...Array.isArray(check.undecided) ? check.undecided : [], ...undecidedSamples.map(name)];
48585
50911
  if (failed.length > 0) return {
48586
50912
  exitCode: 1,
48587
50913
  error: {
@@ -48593,7 +50919,7 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "")
48593
50919
  exitCode: 3,
48594
50920
  error: {
48595
50921
  code: "expectations_undecided",
48596
- message: `${saved}${plural$1(undecided.length, "expectation is", "expectations are")} undecided on this write, which judges them without classifying mail or asking the rule's questions: ${undecided.join(", ")}. Test them with ${test}.`
50922
+ message: `${saved}${plural$1(undecided.length, "expectation is", "expectations are")} undecided on this write, which judges them without classifying mail or asking the rule's questions${undecidedSamples.length > 0 ? ", and leaves made-up emails unjudged" : ""}: ${undecided.join(", ")}. Test them with ${test}.`
48597
50923
  }
48598
50924
  };
48599
50925
  if (check.passed !== true) return {
@@ -48611,6 +50937,14 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "")
48611
50937
  * rule active, and refuses with these same codes, saving nothing.
48612
50938
  */
48613
50939
  const SAVED_UNJUDGED = "The write was saved: the server judges a write before saving only when it saves a version or turns the rule active, so not this one. ";
50940
+ /** What a written do_not_share rule's report says first. */
50941
+ const BLOCKS_SHARING = "This rule blocks sharing (kind do_not_share): while it is active, no rule shares an email it matches.";
50942
+ /** A do_not_share rule's protection_note, as a line, or undefined. */
50943
+ function protectionLine(rule) {
50944
+ if (!isRecord(rule)) return void 0;
50945
+ const note = rule.protection_note;
50946
+ return typeof note === "string" && note.trim() ? `Protection: ${note.trim()}` : void 0;
50947
+ }
48614
50948
  /** What a restore_version write saved, as its answer says. */
48615
50949
  function restoredLine(restored, currentVersion) {
48616
50950
  if (!isRecord(restored) || typeof restored.version !== "number") return void 0;
@@ -48647,21 +50981,31 @@ function writtenEmailRuleReport(data, request) {
48647
50981
  const rule = data;
48648
50982
  const ruleId = typeof rule.id === "string" ? rule.id : "<rule-id>";
48649
50983
  const lines = scopeWarningLines(rule.scope_warnings);
48650
- if (rule.kind === "do_not_share") lines.unshift(`This rule blocks sharing (kind do_not_share): while it is active, no rule shares an email it matches.${typeof rule.state === "string" && rule.state !== "active" ? " It blocks nothing until it is active." : ""}`);
50984
+ if (rule.kind === "do_not_share") {
50985
+ const note = protectionLine(rule);
50986
+ lines.unshift(...note ? [BLOCKS_SHARING, note] : [`${BLOCKS_SHARING}${typeof rule.state === "string" && rule.state !== "active" ? " It blocks nothing until it is active." : ""}`]);
50987
+ }
48651
50988
  const restored = restoredLine(rule.restored_from, rule.current_version);
48652
50989
  if (restored) lines.push(restored);
48653
50990
  const reach = affectsLine(rule.affects);
48654
50991
  if (reach) lines.push(reach);
48655
- const sentExpectations = isRecord(request.body) && isRecord(request.body.expectations);
48656
- const checked = expectationsCheckLine(rule.expectations_check, ruleId);
50992
+ const sentExpectations = sentExpectationsIn(request.body);
50993
+ const name = writtenSampleNamer(request.body, request.flags, rule.expectation_samples);
50994
+ const checked = expectationsCheckLine(rule.expectations_check, ruleId, name);
48657
50995
  if (checked) lines.push(checked);
48658
50996
  else if (sentExpectations) lines.push(`Expectations were not checked on this write. Test them with \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.`);
48659
50997
  if (request.flags?.["fail-on-expectations"] !== true) return { lines };
48660
50998
  return {
48661
50999
  lines,
48662
- ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId, isUpdate(request) ? SAVED_UNJUDGED : "")
51000
+ ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId, isUpdate(request) ? SAVED_UNJUDGED : "", name)
48663
51001
  };
48664
51002
  }
51003
+ /** Whether a rule write sent expectations: emails by id, or made-up emails to save or add. */
51004
+ function sentExpectationsIn(body) {
51005
+ if (!isRecord(body)) return false;
51006
+ const some = (value) => Array.isArray(value) && value.length > 0;
51007
+ return isRecord(body.expectations) || some(body.expectation_samples) || some(body.add_expectation_samples);
51008
+ }
48665
51009
  /** Whether a rule write is an update: it names the rule in its path. */
48666
51010
  function isUpdate(request) {
48667
51011
  return typeof request.path?.id === "string";
@@ -48673,7 +51017,7 @@ function isUpdate(request) {
48673
51017
  */
48674
51018
  function writtenFallbackExitCode(data, request) {
48675
51019
  if (request.flags["fail-on-expectations"] !== true) return void 0;
48676
- return writtenExpectationsOutcome(isRecord(data) ? data.expectations_check : void 0, isRecord(request.body) && isRecord(request.body.expectations), "<rule-id>").exitCode;
51020
+ return writtenExpectationsOutcome(isRecord(data) ? data.expectations_check : void 0, sentExpectationsIn(request.body), "<rule-id>").exitCode;
48677
51021
  }
48678
51022
  /** The stderr report of get-email-rule-scope. */
48679
51023
  function emailRuleScopeReport(data) {
@@ -48740,10 +51084,10 @@ function readAgain(id) {
48740
51084
  * The hint for update-email-rule's refusals: an email_rule_conflict by its
48741
51085
  * details.reason (version_moved, revision_moved, expectations_moved,
48742
51086
  * rule_changed), and the expectations guard's expectations_failed,
48743
- * expectations_undecided and no_expectations. Nothing was saved in any of
48744
- * them.
51087
+ * expectations_undecided, expectations_unreadable and no_expectations.
51088
+ * Nothing was saved in any of them.
48745
51089
  */
48746
- function updateEmailRuleErrorHint(code, payload, ruleId, body) {
51090
+ function updateEmailRuleErrorHint(code, payload, ruleId, body, flags) {
48747
51091
  const id = typeof ruleId === "string" && ruleId ? ruleId : "<rule-id>";
48748
51092
  const details = errorDetails(payload);
48749
51093
  const guarded = isRecord(body) && body.if_expectations_pass === true;
@@ -48761,10 +51105,50 @@ function updateEmailRuleErrorHint(code, payload, ruleId, body) {
48761
51105
  const check = isRecord(details.expectations_check) ? details.expectations_check : {};
48762
51106
  const failed = Array.isArray(check.failed) ? check.failed : [];
48763
51107
  const undecided = Array.isArray(check.undecided) ? check.undecided : [];
48764
- const parts = [...failed.length > 0 ? [`failed: ${failed.join(", ")}`] : [], ...undecided.length > 0 ? [`undecided: ${undecided.join(", ")}`] : []];
48765
- return `Hint: the rule, as this write would leave it, was judged against its expectations before saving (if_expectations_pass), and nothing was saved.${parts.length > 0 ? ` Expectations ${parts.join("; ")}.` : ""}${code === "expectations_failed" ? "" : ` ${UNDECIDED_NEXT_STEP[String(details.reason)] ?? UNDECIDED_NEXT_STEP.none}`} The command exits ${updateEmailRuleErrorExitCode(payload) ?? 1}.`;
51108
+ const name = writtenSampleNamer(body, flags);
51109
+ const failedAll = [...failed, ...indexList(check.failed_samples).map(name)];
51110
+ const undecidedSamples = indexList(check.undecided_samples);
51111
+ const undecidedAll = [...undecided, ...undecidedSamples.map(name)];
51112
+ const parts = [...failedAll.length > 0 ? [`failed: ${failedAll.join(", ")}`] : [], ...undecidedAll.length > 0 ? [`undecided: ${undecidedAll.join(", ")}`] : []];
51113
+ const addedOrder = isRecord(body) && Array.isArray(body.add_expectation_samples) && indexList(check.failed_samples).length + undecidedSamples.length > 0 ? " Made-up emails are numbered from 1: the rule's saved ones first, then those this write adds." : "";
51114
+ const which = parts.length > 0 ? ` Expectations ${parts.join("; ")}.${addedOrder}` : "";
51115
+ const reason = String(details.reason);
51116
+ const samplesMayStay = undecidedSamplesMayStay(undecidedSamples.length, reason);
51117
+ const byReason = reason !== "none" && Object.hasOwn(UNDECIDED_NEXT_STEP, reason) ? UNDECIDED_NEXT_STEP[reason] : UNDECIDED_NEXT_STEP.none;
51118
+ return `Hint: the rule, as this write would leave it, was judged against its expectations before saving (if_expectations_pass), and nothing was saved.${which}${code === "expectations_failed" ? "" : samplesMayStay ? `${undecided.length > 0 ? ` ${byReason}` : ""} ${UNDECIDED_SAMPLES_NEXT_STEP} ${refusedSampleTest(body, id)}` : ` ${byReason}`} The command exits ${updateEmailRuleErrorExitCode(payload) ?? 1}.`;
48766
51119
  }
48767
- if (code === "no_expectations") return "Hint: nothing was saved. Give the rule expectations with --expectations-file (in this write or before it), or send the write without if_expectations_pass (--fail-on-expectations or --if-expectations-pass).";
51120
+ if (code === "expectations_unreadable") {
51121
+ const unreadable = Array.isArray(details.unreadable) ? details.unreadable.filter((entry) => entry !== null && entry !== void 0 && entry !== "").map(String) : [];
51122
+ const one = unreadable.length === 1;
51123
+ const named = unreadable.length > 0 ? `: ${unreadable.join(", ")}` : "";
51124
+ const them = one ? "it" : "them";
51125
+ return `Hint: the rule, as this write would leave it, was judged against its expectations before saving (if_expectations_pass), and nothing was saved. ${one ? "An expectation names an email" : "Expectations name emails"} this rule cannot read (deleted, or outside its scope or its author's mail)${named}. Sending the write again will not decide ${them}: send --expectations-file without ${them}, or an evaluation_scope that reads ${them}. The command exits 1.`;
51126
+ }
51127
+ if (code === "no_expectations") return "Hint: nothing was saved. Give the rule expectations with --expectations-file or made-up emails with --expectation-samples-file or --add-expectation-samples-file (in this write or before it), or send the write without if_expectations_pass (--fail-on-expectations or --if-expectations-pass).";
51128
+ }
51129
+ /** The reasons a guard's undecided judgment can be decided by sending the write again. */
51130
+ const RETRYABLE_REASONS = /* @__PURE__ */ new Set([
51131
+ "deadline",
51132
+ "error",
51133
+ "client_gone"
51134
+ ]);
51135
+ /**
51136
+ * A made-up email can stay undecided: one that reads a spam score it does
51137
+ * not carry, or whose classification failed or left out a fact the rule
51138
+ * needs, decides the same way every time.
51139
+ */
51140
+ const UNDECIDED_SAMPLES_NEXT_STEP = "A made-up email undecided for no reason given may stay so (a spam score it does not carry, a fact its classification left out, or a failed classification), so sending the write again may not decide it.";
51141
+ /**
51142
+ * How to see why a refused write's made-up emails are undecided: only
51143
+ * test-email-rule-on-sample answers with what a made-up email lacked. Test
51144
+ * the tree this write sent (or the version it restores), else the rule, on
51145
+ * each. Nothing was saved, so a test by rule id would see neither.
51146
+ */
51147
+ function refusedSampleTest(body, ruleId) {
51148
+ const onSample = "`primitive email-rules test-email-rule-on-sample`";
51149
+ if (isRecord(body) && isRecord(body.tree)) return `To see why, test the tree this write sent on each with ${onSample}.`;
51150
+ if (isRecord(body) && typeof body.restore_version === "number") return `To see why, test version ${body.restore_version}'s tree (\`primitive email-rules list-email-rule-versions ${ruleId} --versions ${body.restore_version}\`) on each with ${onSample}.`;
51151
+ return `To see why, test rule ${ruleId} on each with ${onSample}${isRecord(body) && Array.isArray(body.expectation_samples) ? "" : ` (\`primitive email-rules get-email-rule ${ruleId}\` lists its saved ones)`}.`;
48768
51152
  }
48769
51153
  /** What to do about an undecided judgment, by details.reason ("none": no reason given). */
48770
51154
  const UNDECIDED_NEXT_STEP = {
@@ -48776,21 +51160,34 @@ const UNDECIDED_NEXT_STEP = {
48776
51160
  };
48777
51161
  /**
48778
51162
  * update-email-rule's exit code for the expectations guard's refusals: 1 when
48779
- * one failed, or when they cannot be decided by trying again (the rule cannot
48780
- * run as its author); 3 when trying again later can decide them.
51163
+ * one failed, or when they may not be decided by trying again (an expectation
51164
+ * names an email the rule cannot read, the rule cannot run as its author, or
51165
+ * a made-up email is undecided for no reason the answer gives); 3 when trying
51166
+ * again later can decide them.
48781
51167
  */
48782
51168
  function updateEmailRuleErrorExitCode(payload) {
48783
51169
  const code = isRecord(payload) ? payload.code : void 0;
48784
- if (code === "expectations_failed") return 1;
51170
+ if (code === "expectations_failed" || code === "expectations_unreadable") return 1;
48785
51171
  if (code !== "expectations_undecided") return void 0;
48786
- return errorDetails(payload).reason === "author_unresolvable" ? 1 : 3;
51172
+ const details = errorDetails(payload);
51173
+ if (details.reason === "author_unresolvable") return 1;
51174
+ return undecidedSamplesMayStay(indexList((isRecord(details.expectations_check) ? details.expectations_check : {}).undecided_samples).length, String(details.reason)) ? 1 : 3;
51175
+ }
51176
+ /**
51177
+ * Whether a guard's undecided made-up emails may stay undecided: some are,
51178
+ * and the answer gives no reason that sending the write again can change
51179
+ * (author_unresolvable has its own advice and exit code).
51180
+ */
51181
+ function undecidedSamplesMayStay(count, reason) {
51182
+ return count > 0 && !RETRYABLE_REASONS.has(reason) && reason !== "author_unresolvable";
48787
51183
  }
48788
51184
  /** The stderr report of compile-email-rule: how many recent emails it holds for. */
48789
51185
  function compileEmailRuleReport(data) {
48790
51186
  if (!isRecord(data)) return { lines: [] };
51187
+ const lines = (Array.isArray(data.warnings) ? data.warnings : []).filter((warning) => typeof warning === "string" && warning.trim() !== "").map((warning) => `Warning: ${warning.trim()}`);
48791
51188
  const breadth = data.breadth;
48792
- if (!isRecord(breadth) || typeof breadth.checked !== "number" || typeof breadth.matched !== "number") return { lines: [] };
48793
- return { lines: [`Breadth: its conditions hold for ${breadth.matched} of ${plural$1(breadth.checked, "recent email", "recent emails")} checked in your own mailboxes, whatever scope you save it with.`] };
51189
+ if (isRecord(breadth) && typeof breadth.checked === "number" && typeof breadth.matched === "number") lines.push(`Breadth: its conditions hold for ${breadth.matched} of ${plural$1(breadth.checked, "recent email", "recent emails")} checked in your own mailboxes, whatever scope you save it with.`);
51190
+ return { lines };
48794
51191
  }
48795
51192
  /** The stderr report of get-email-rule: who wrote it, and its revision. */
48796
51193
  function getEmailRuleReport(data) {
@@ -48798,13 +51195,31 @@ function getEmailRuleReport(data) {
48798
51195
  const lines = [];
48799
51196
  const author = authorLine(data.author);
48800
51197
  if (author) lines.push(author);
51198
+ const protection = protectionLine(data);
51199
+ if (protection) lines.push(protection);
48801
51200
  if (typeof data.revision === "number") lines.push(`Revision ${data.revision}: pass --if-revision ${data.revision} to update-email-rule so a change made since is never replaced unseen.`);
48802
51201
  return { lines };
48803
51202
  }
51203
+ /** The stderr report of list-email-rules: the protective rules that protect nothing yet, or less than they seem. */
51204
+ function listEmailRulesReport(data) {
51205
+ const rules = Array.isArray(data) ? data : [];
51206
+ const lines = [];
51207
+ for (const rule of rules) {
51208
+ if (!isRecord(rule)) continue;
51209
+ const note = typeof rule.protection_note === "string" ? rule.protection_note.trim() : "";
51210
+ if (!note) continue;
51211
+ const id = typeof rule.id === "string" ? rule.id : "<rule-id>";
51212
+ const name = typeof rule.name === "string" ? rule.name : "no name";
51213
+ lines.push(`Protection of ${id} (${name}): ${note}`);
51214
+ }
51215
+ return { lines };
51216
+ }
48804
51217
  /** create-email-rule's --fail-on-expectations: the exit code follows the check made after saving. */
48805
- const FAIL_ON_EXPECTATIONS_HELP = "Exit 1 when an expectation in the answer's expectations_check failed, and 3 when none failed but one is undecided or the expectations this write sent were not checked (test the rule to decide). Without it the command exits 0 whatever the check says.";
51218
+ const FAIL_ON_EXPECTATIONS_HELP = "Exit 1 when an expectation in the answer's expectations_check failed, and 3 when none failed but one is undecided or the expectations this write sent were not checked (test the rule to decide). A create does not judge made-up emails, so with some it exits 3. Without it the command exits 0 whatever the check says.";
48806
51219
  /** update-email-rule's --fail-on-expectations: the server refuses the write. */
48807
- const UPDATE_FAIL_ON_EXPECTATIONS_HELP = "Refuse the write unless the rule, as this write would leave it, passes its expectations. It sends if_expectations_pass (over any in --body-file or --raw-body), so the server judges a new version, a restored one, a new scope or turning the rule active before saving, and saves nothing when one failed (exit 1), one is undecided (exit 3, or 1 when the rule cannot run as its author) or the rule has none (exit 1). It can take as long as a test of the rule; the command waits for it. Judging needs a credential that reads the mail's content. A write the server does not judge (one changing only expectations, say) is saved, and the exit code then follows the answer's expectations_check, as on create. With --json, a refused write's document holds the API's error alone; a saved write's holds the rule.";
51220
+ const UPDATE_FAIL_ON_EXPECTATIONS_HELP = "Refuse the write unless the rule, as this write would leave it, passes its expectations. It sends if_expectations_pass (over any in --body-file or --raw-body), so the server judges a new version, a restored one, a new scope or turning the rule active before saving, on its emails by id and its made-up emails, and saves nothing when one failed (exit 1), one names an email the rule cannot read (exit 1, since sending again never decides it), one is undecided (exit 3, or 1 when the rule cannot run as its author or a made-up email is undecided for no reason the answer gives) or the rule has none (exit 1). It can take as long as a test of the rule; the command waits for it. Judging needs a credential that reads the mail's content. A write the server does not judge (one changing only expectations, say) is saved, and the exit code then follows the answer's expectations_check, as on create. With --json, a refused write's document holds the API's error alone; a saved write's holds the rule.";
51221
+ /** What a samples file flag reads. */
51222
+ const SAMPLES_FILE_SHAPE = "A JSON file of made-up emails the rule must match or not: an array such as [{\"expected\": \"no_match\", \"label\": \"a personal bill\", \"email\": {...}}], with email as test-email-rule-on-sample takes it (text up to 4,000 characters, at most 10 each of to, cc and attachment_types), like the one generate-look-alikes --samples-file writes, or a generate-look-alikes answer saved as it printed it.";
48808
51223
  const EXPECTATION_FLAG_NOTE = "Replaces any expect in --body-file or --raw-body. The answer reports them in expectations (passed, counts, results) and the exit code follows it; see --help-full.";
48809
51224
  /**
48810
51225
  * update-email-rule's --fail-on-expectations sends if_expectations_pass, over
@@ -48819,6 +51234,15 @@ function expectationsGuardBodyFields(flags) {
48819
51234
  value: true
48820
51235
  }];
48821
51236
  }
51237
+ /**
51238
+ * The stderr report of end-email-rule-share: the answer's note, which says
51239
+ * the rule itself is left as it was (ending a share does not turn it off)
51240
+ * and, to the member who shared, how to stop it.
51241
+ */
51242
+ function endEmailRuleShareReport(data) {
51243
+ const note = isRecord(data) && typeof data.note === "string" ? data.note.trim() : "";
51244
+ return { lines: note ? [note] : [] };
51245
+ }
48822
51246
  const EMAIL_RULE_OPERATION_EXTENSIONS = {
48823
51247
  testEmailRule: {
48824
51248
  flags: {
@@ -48833,9 +51257,11 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
48833
51257
  exclusive: ["expect-file"]
48834
51258
  }),
48835
51259
  "expect-file": Flags.string({
48836
- description: "A JSON file holding the request's expect, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}. Not with --expect-match or --expect-no-match.",
51260
+ description: "A JSON file holding the emails the request's expect names by id, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}; made-up emails go in --expect-samples-file. Not with --expect-match or --expect-no-match.",
48837
51261
  exclusive: ["expect-match", "expect-no-match"]
48838
- })
51262
+ }),
51263
+ [EXPECT_SAMPLES_FILE_FLAG]: Flags.string({ description: `${SAMPLES_FILE_SHAPE} Sets expect.samples (at most 20), beside any emails by id; the answer judges each in expectations.samples (index from 0 over those sent), and the report names each by its number in the file. Like any expect, it replaces a saved rule's own expectations and samples for this test. ${EXPECTATION_FLAG_NOTE}` }),
51264
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([EXPECT_SAMPLES_FILE_FLAG]) })
48839
51265
  },
48840
51266
  bodyFields: expectBodyFields,
48841
51267
  bodyProperties: ["expect"],
@@ -48848,32 +51274,71 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
48848
51274
  createEmailRule: {
48849
51275
  flags: {
48850
51276
  "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check (passed, decided, failed, undecided); the report test-email-rule answers with is named expectations instead." }),
51277
+ [SAMPLES_FILE_FLAG]: Flags.string({ description: `${SAMPLES_FILE_SHAPE} Sets expectation_samples (at most 20), over any in --body-file or --raw-body. A create does not judge them; test-email-rule --rule-id does.` }),
51278
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([SAMPLES_FILE_FLAG]) }),
48851
51279
  [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: FAIL_ON_EXPECTATIONS_HELP })
48852
51280
  },
48853
- bodyFields: expectationsBodyFields(false),
48854
- bodyProperties: ["expectations"],
51281
+ bodyFields: (flags) => [...expectationsBodyFields(false)(flags), ...samplesBodyFields(false)(flags)],
51282
+ bodyProperties: ["expectations", "expectation_samples"],
48855
51283
  fallbackExitCode: writtenFallbackExitCode,
48856
51284
  report: writtenEmailRuleReport
48857
51285
  },
48858
51286
  updateEmailRule: {
48859
51287
  flags: {
48860
51288
  "expectations-file": Flags.string({ description: "A JSON file holding the emails the rule must match and must not, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}, replacing its saved expectations; a file holding null removes them. Sets expectations, over any in --body-file or --raw-body; the answer judges them in expectations_check (passed, decided, failed, undecided); the report test-email-rule answers with is named expectations instead." }),
51289
+ [SAMPLES_FILE_FLAG]: Flags.string({
51290
+ description: `${SAMPLES_FILE_SHAPE} They replace the rule's saved ones, and a file holding [] removes them. Sets expectation_samples, over any in --body-file or --raw-body. Not with --${ADD_SAMPLES_FILE_FLAG}. The write judges them only with --${FAIL_ON_EXPECTATIONS_FLAG}, and only when it saves a version or turns the rule active.`,
51291
+ exclusive: [ADD_SAMPLES_FILE_FLAG]
51292
+ }),
51293
+ [ADD_SAMPLES_FILE_FLAG]: Flags.string({
51294
+ description: `${SAMPLES_FILE_SHAPE} They are added to the rule's saved ones, 20 at most in all. Sets add_expectation_samples, over any in --body-file or --raw-body. Not with --${SAMPLES_FILE_FLAG}. The write judges them only with --${FAIL_ON_EXPECTATIONS_FLAG}, and only when it saves a version or turns the rule active.`,
51295
+ exclusive: [SAMPLES_FILE_FLAG]
51296
+ }),
51297
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([SAMPLES_FILE_FLAG, ADD_SAMPLES_FILE_FLAG]) }),
48861
51298
  [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: UPDATE_FAIL_ON_EXPECTATIONS_HELP })
48862
51299
  },
48863
- bodyFields: (flags) => [...expectationsBodyFields(true)(flags), ...expectationsGuardBodyFields(flags)],
48864
- bodyProperties: ["expectations"],
51300
+ bodyFields: (flags) => [
51301
+ ...expectationsBodyFields(true)(flags),
51302
+ ...samplesBodyFields(true)(flags),
51303
+ ...expectationsGuardBodyFields(flags)
51304
+ ],
51305
+ bodyProperties: [
51306
+ "expectations",
51307
+ "expectation_samples",
51308
+ "add_expectation_samples"
51309
+ ],
48865
51310
  errorExitCode: updateEmailRuleErrorExitCode,
48866
51311
  fallbackExitCode: writtenFallbackExitCode,
48867
51312
  report: writtenEmailRuleReport
48868
51313
  },
51314
+ generateLookAlikes: {
51315
+ flags: { [LOOK_ALIKES_SAMPLES_FILE_FLAG]: Flags.string({ description: `Also write the look-alikes to this JSON file as made-up emails the rule must match or not (each keeps its expected: most must not match, and a near case it should take must match), a JSON array numbered from 1 in the order the report prints them. Save the ones you choose with create-email-rule --${SAMPLES_FILE_FLAG}, update-email-rule --${ADD_SAMPLES_FILE_FLAG} or, for a request, accept-email-rule-request --${SAMPLES_FILE_FLAG}, with --pick; test an edited rule on them with test-email-rule --${EXPECT_SAMPLES_FILE_FLAG}. Replaces the file if it exists, once the call succeeds; when no look-alikes came back it leaves the file as it was and exits 1. The numbered report is printed on stderr, and only without --json, which prints exactly one JSON document and nothing on stderr; the answer's look_alikes and this file hold the same emails in the same order, so with --json the file still gives the numbered set to --pick from.` }) },
51316
+ report: lookAlikesReport
51317
+ },
51318
+ acceptEmailRuleRequest: {
51319
+ flags: {
51320
+ [SAMPLES_FILE_FLAG]: Flags.string({ description: `${SAMPLES_FILE_SHAPE} Sets expectation_samples (at most 20), over any in --body-file or --raw-body: they are kept on the rule this accept writes, such as the look-alikes of the request, each keeping whether it must match.` }),
51321
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([SAMPLES_FILE_FLAG]) })
51322
+ },
51323
+ bodyFields: samplesBodyFields(false),
51324
+ bodyProperties: ["expectation_samples"]
51325
+ },
48869
51326
  compileEmailRule: {
48870
51327
  flags: {},
48871
51328
  report: compileEmailRuleReport
48872
51329
  },
51330
+ endEmailRuleShare: {
51331
+ flags: {},
51332
+ report: endEmailRuleShareReport
51333
+ },
48873
51334
  getEmailRule: {
48874
51335
  flags: {},
48875
51336
  report: getEmailRuleReport
48876
51337
  },
51338
+ listEmailRules: {
51339
+ flags: {},
51340
+ report: listEmailRulesReport
51341
+ },
48877
51342
  getEmailRuleScope: {
48878
51343
  flags: {},
48879
51344
  report: emailRuleScopeReport
@@ -51004,7 +53469,7 @@ function savedConnectedProfiles(configDir, exclude) {
51004
53469
  */
51005
53470
  async function otherProfiles(configDir, session, current) {
51006
53471
  if (session) try {
51007
- const { connectedProfilesForSession } = await import("./machine-session-YQWxPePp.js").then((n) => n.s);
53472
+ const { connectedProfilesForSession } = await import("./machine-session-Cre9Mt_2.js").then((n) => n.s);
51008
53473
  return {
51009
53474
  inSession: true,
51010
53475
  profiles: connectedProfilesForSession(configDir, session, current)
@@ -52111,7 +54576,7 @@ const OPERATION_ERROR_HINTS = {
52111
54576
  inviteOrganizationMember: (code, body, payload) => inviteRefusalHint(code, body, payload),
52112
54577
  createEndpoint: (code, body, payload) => createEndpointRuleRequestHint(code, body, payload),
52113
54578
  updateEndpoint: (code, body, payload) => updateEndpointRuleRequestHint(code, body, payload),
52114
- updateEmailRule: (code, body, payload, path) => updateEmailRuleErrorHint(code, payload, path?.id, body),
54579
+ updateEmailRule: (code, body, payload, path, flags) => updateEmailRuleErrorHint(code, payload, path?.id, body, flags),
52115
54580
  verifyDomain: (code, _body, payload) => code === "dns_verification_failed" ? domainVerificationFailedHint(payload) : void 0,
52116
54581
  addDomain: (code, body) => {
52117
54582
  if (code === "mail_relay_unavailable") return MAIL_RELAY_UNAVAILABLE_MESSAGE;
@@ -52314,7 +54779,7 @@ function createOperationCommand(operation) {
52314
54779
  }) : await callOperation();
52315
54780
  if (result.error) {
52316
54781
  const errorPayload = extractErrorPayload(result.error);
52317
- const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload, collectValues(operation.pathParams, parsedFlags, operation.sdkName));
54782
+ const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload, collectValues(operation.pathParams, parsedFlags, operation.sdkName), parsedFlags);
52318
54783
  writeErrorWithHints(errorPayload, result.response, { codeHint: !(operationErrorHint && OPERATION_ERROR_HINTS_REPLACING_CODE_HINT.has(operation.sdkName)) });
52319
54784
  if (operationErrorHint) process.stderr.write(`${operationErrorHint}\n`);
52320
54785
  if (stepUpUrl) process.stderr.write(approveThenRerunHint(stepUpUrl));
@@ -52399,7 +54864,7 @@ function createOperationCommand(operation) {
52399
54864
  ...tail ? { pastFlag: "--since" } : query.cursor !== void 0 ? { pastFlag: "--cursor" } : {},
52400
54865
  nextCursor: tail ? null : more,
52401
54866
  caller: tail || more ? null : await readCallerMailFor(auth, apiClient.client)
52402
- }) : EMPTY_RESULT_HINTS[operation.sdkName];
54867
+ }) : emptyRuleListHint(operation.sdkName, query) ?? EMPTY_RESULT_HINTS[operation.sdkName];
52403
54868
  if (hint && jsonOutput) summary.push(hint);
52404
54869
  else if (hint) process.stderr.write(`${hint}\n`);
52405
54870
  }
@@ -52476,6 +54941,30 @@ const EMPTY_RESULT_HINTS = {
52476
54941
  listFilters: "(no results) No filter rules configured.",
52477
54942
  listFunctions: "(no results) No Functions configured yet. Start with `primitive functions templates`, then `primitive functions init --template <template>` and `primitive functions deploy --name <name> --file <bundle>`."
52478
54943
  };
54944
+ /**
54945
+ * An empty rule request or share list, read by the filters the call left at
54946
+ * their defaults: each list shows only part of what you can see by default
54947
+ * (pending requests made to you; live shares made with you), so an empty
54948
+ * answer says which flags widen it. Undefined for any other list, when every
54949
+ * filter was given, or on a later page (--cursor), which is empty only past
54950
+ * the end.
54951
+ */
54952
+ function emptyRuleListHint(sdkName, query) {
54953
+ if (query.cursor !== void 0) return void 0;
54954
+ const unset = (name) => query[name] === void 0;
54955
+ const said = (noun, phrase, flags) => phrase === null ? void 0 : `(no results) No ${noun} match. By default this lists only ${phrase}: add \`${flags.join(" ")}\` to widen it.`;
54956
+ if (sdkName === "listEmailRuleRequests") {
54957
+ const box = unset("box");
54958
+ const status = unset("status");
54959
+ return said("requests", box && status ? "pending requests made to you" : box ? "requests made to you" : status ? "pending requests" : null, [...box ? ["--box all"] : [], ...status ? ["--status any"] : []]);
54960
+ }
54961
+ if (sdkName === "listEmailRuleShares") {
54962
+ const box = unset("box");
54963
+ const live = unset("live");
54964
+ return said("shares", box && live ? "live shares made with you" : box ? "shares made with you" : live ? "live shares" : null, [...box ? ["--box all"] : [], ...live ? ["--live any"] : []]);
54965
+ }
54966
+ if (sdkName === "listGivenEmailRuleShares") return said("shares", unset("live") ? "live shares of your mail" : null, ["--live any"]);
54967
+ }
52479
54968
  function canonicalizeCliReferences(description) {
52480
54969
  return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
52481
54970
  }