primcli 3.0.0-preview.6 → 3.0.0-preview.8

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,6 +1,7 @@
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-CP0h4alF.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-DNbrLjEF.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
+ import { a as thrownErrorBody } from "./json-output-ch756TXV.js";
4
5
  import { d as readMailJson, h as writeMailJson, m as withMailLock, p as syncMailDirectory, u as privateMailPermissions } from "./shared-mail-files-BAQSGxxz.js";
5
6
  import { Args, Command, Errors, Flags } from "@oclif/core";
6
7
  import { existsSync, lstatSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
@@ -10556,7 +10557,7 @@ const operationManifest = [
10556
10557
  "binaryResponse": false,
10557
10558
  "bodyRequired": true,
10558
10559
  "command": "revoke-credential",
10559
- "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.",
10560
10561
  "hasJsonBody": true,
10561
10562
  "method": "POST",
10562
10563
  "operationId": "revokeCredential",
@@ -10669,7 +10670,7 @@ const operationManifest = [
10669
10670
  }
10670
10671
  },
10671
10672
  "already_revoked": {
10672
- "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.",
10673
10674
  "type": "boolean"
10674
10675
  },
10675
10676
  "keep_failed": {
@@ -10702,6 +10703,18 @@ const operationManifest = [
10702
10703
  "org_owned": {
10703
10704
  "type": "boolean",
10704
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"]
10705
10718
  }
10706
10719
  },
10707
10720
  "required": [
@@ -10712,6 +10725,28 @@ const operationManifest = [
10712
10725
  ],
10713
10726
  "additionalProperties": {}
10714
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" }
10715
10750
  }
10716
10751
  },
10717
10752
  "required": [
@@ -10730,7 +10765,7 @@ const operationManifest = [
10730
10765
  "binaryResponse": false,
10731
10766
  "bodyRequired": true,
10732
10767
  "command": "rotate-key",
10733
- "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.",
10734
10769
  "hasJsonBody": true,
10735
10770
  "method": "POST",
10736
10771
  "operationId": "rotateKey",
@@ -10904,7 +10939,7 @@ const operationManifest = [
10904
10939
  "binaryResponse": false,
10905
10940
  "bodyRequired": true,
10906
10941
  "command": "update-key",
10907
- "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.",
10908
10943
  "hasJsonBody": true,
10909
10944
  "method": "PATCH",
10910
10945
  "operationId": "updateKey",
@@ -12916,7 +12951,7 @@ const operationManifest = [
12916
12951
  "binaryResponse": false,
12917
12952
  "bodyRequired": false,
12918
12953
  "command": "accept-email-rule-request",
12919
- "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.",
12954
+ "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.",
12920
12955
  "hasJsonBody": true,
12921
12956
  "method": "POST",
12922
12957
  "operationId": "acceptEmailRuleRequest",
@@ -13014,6 +13049,131 @@ const operationManifest = [
13014
13049
  "type": "string",
13015
13050
  "minLength": 1,
13016
13051
  "maxLength": 64
13052
+ },
13053
+ "breadth": {
13054
+ "description": "Ignored (the compile answer's breadth).",
13055
+ "anyOf": [{
13056
+ "type": "object",
13057
+ "properties": {
13058
+ "checked": {
13059
+ "type": "integer",
13060
+ "minimum": 0,
13061
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
13062
+ },
13063
+ "matched": {
13064
+ "type": "integer",
13065
+ "minimum": 0,
13066
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
13067
+ }
13068
+ },
13069
+ "required": ["checked", "matched"]
13070
+ }, { "type": "null" }]
13071
+ },
13072
+ "expectation_samples": {
13073
+ "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.",
13074
+ "minItems": 1,
13075
+ "maxItems": 20,
13076
+ "type": "array",
13077
+ "items": {
13078
+ "type": "object",
13079
+ "properties": {
13080
+ "expected": {
13081
+ "type": "string",
13082
+ "enum": ["match", "no_match"]
13083
+ },
13084
+ "label": {
13085
+ "type": ["string", "null"],
13086
+ "minLength": 1,
13087
+ "maxLength": 200,
13088
+ "pattern": "^[^\\u0000]*$",
13089
+ "description": "What the email stands for, such as a personal medical bill."
13090
+ },
13091
+ "email": {
13092
+ "type": "object",
13093
+ "properties": {
13094
+ "from": {
13095
+ "type": "object",
13096
+ "properties": {
13097
+ "address": {
13098
+ "type": "string",
13099
+ "maxLength": 254,
13100
+ "format": "email",
13101
+ "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,}$"
13102
+ },
13103
+ "name": {
13104
+ "type": "string",
13105
+ "maxLength": 200,
13106
+ "pattern": "^[^\\u0000]*$"
13107
+ }
13108
+ },
13109
+ "required": ["address"]
13110
+ },
13111
+ "to": {
13112
+ "minItems": 1,
13113
+ "maxItems": 10,
13114
+ "type": "array",
13115
+ "items": {
13116
+ "type": "string",
13117
+ "maxLength": 254,
13118
+ "format": "email",
13119
+ "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,}$"
13120
+ }
13121
+ },
13122
+ "cc": {
13123
+ "maxItems": 10,
13124
+ "type": "array",
13125
+ "items": {
13126
+ "type": "string",
13127
+ "maxLength": 254,
13128
+ "format": "email",
13129
+ "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,}$"
13130
+ }
13131
+ },
13132
+ "subject": {
13133
+ "type": "string",
13134
+ "maxLength": 998,
13135
+ "pattern": "^[^\\u0000]*$"
13136
+ },
13137
+ "text": {
13138
+ "type": "string",
13139
+ "minLength": 1,
13140
+ "maxLength": 4e3,
13141
+ "pattern": "^[^\\u0000]*$"
13142
+ },
13143
+ "attachment_types": {
13144
+ "maxItems": 10,
13145
+ "type": "array",
13146
+ "items": {
13147
+ "type": "string",
13148
+ "minLength": 1,
13149
+ "maxLength": 255,
13150
+ "pattern": "^[^\\u0000]*$"
13151
+ }
13152
+ },
13153
+ "automated": { "type": "boolean" },
13154
+ "from_mailing_list": { "type": "boolean" },
13155
+ "spam_score": {
13156
+ "type": "number",
13157
+ "minimum": -1e3,
13158
+ "maximum": 1e3
13159
+ }
13160
+ },
13161
+ "required": [
13162
+ "from",
13163
+ "to",
13164
+ "text"
13165
+ ],
13166
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
13167
+ },
13168
+ "id": {
13169
+ "type": "string",
13170
+ "format": "uuid"
13171
+ },
13172
+ "created_at": { "type": "string" }
13173
+ },
13174
+ "required": ["expected", "email"],
13175
+ "additionalProperties": false
13176
+ }
13017
13177
  }
13018
13178
  },
13019
13179
  "additionalProperties": false
@@ -13168,6 +13328,14 @@ const operationManifest = [
13168
13328
  "decline_reason": {
13169
13329
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
13170
13330
  "type": ["string", "null"]
13331
+ },
13332
+ "edited_on_accept": {
13333
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
13334
+ "type": ["boolean", "null"]
13335
+ },
13336
+ "notice": {
13337
+ "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.",
13338
+ "type": ["string", "null"]
13171
13339
  }
13172
13340
  },
13173
13341
  "required": [
@@ -13214,6 +13382,10 @@ const operationManifest = [
13214
13382
  "type": "string",
13215
13383
  "enum": ["grant", "do_not_share"]
13216
13384
  },
13385
+ "protection_note": {
13386
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), or a classifier fact narrows it.",
13387
+ "type": "string"
13388
+ },
13217
13389
  "evaluation_scope": {
13218
13390
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
13219
13391
  "type": "string",
@@ -13244,10 +13416,37 @@ const operationManifest = [
13244
13416
  "type": ["integer", "null"],
13245
13417
  "exclusiveMinimum": 0
13246
13418
  },
13419
+ "revision": {
13420
+ "description": "Moves on by one with every update. Send it as if_revision.",
13421
+ "type": "integer",
13422
+ "exclusiveMinimum": 0
13423
+ },
13247
13424
  "author_user_id": {
13248
13425
  "type": "string",
13249
13426
  "format": "uuid"
13250
13427
  },
13428
+ "author": {
13429
+ "type": "object",
13430
+ "properties": {
13431
+ "user_id": {
13432
+ "type": "string",
13433
+ "format": "uuid"
13434
+ },
13435
+ "name": {
13436
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
13437
+ "type": ["string", "null"]
13438
+ },
13439
+ "email": {
13440
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
13441
+ "type": ["string", "null"]
13442
+ }
13443
+ },
13444
+ "required": [
13445
+ "user_id",
13446
+ "name",
13447
+ "email"
13448
+ ]
13449
+ },
13251
13450
  "is_author": {
13252
13451
  "type": "boolean",
13253
13452
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -13260,91 +13459,94 @@ const operationManifest = [
13260
13459
  "type": "string",
13261
13460
  "format": "date-time"
13262
13461
  },
13263
- "current": { "anyOf": [{
13264
- "type": "object",
13265
- "properties": {
13266
- "version": {
13267
- "type": "integer",
13268
- "exclusiveMinimum": 0
13269
- },
13270
- "english": { "type": "string" },
13271
- "tree": {
13272
- "type": "object",
13273
- "properties": {
13274
- "version": {
13275
- "type": "number",
13276
- "const": 1
13277
- },
13278
- "vocabulary": {
13279
- "type": "string",
13280
- "description": "Vocabulary version, such as v1."
13281
- },
13282
- "when": {
13283
- "type": "object",
13284
- "propertyNames": { "type": "string" },
13285
- "additionalProperties": {},
13286
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
13287
- },
13288
- "questions": {
13289
- "type": "array",
13290
- "items": { "type": "string" },
13291
- "description": "Yes/no questions asked once the conditions match; all must be yes."
13292
- }
13462
+ "current": {
13463
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
13464
+ "anyOf": [{
13465
+ "type": "object",
13466
+ "properties": {
13467
+ "version": {
13468
+ "type": "integer",
13469
+ "exclusiveMinimum": 0
13293
13470
  },
13294
- "required": [
13295
- "version",
13296
- "vocabulary",
13297
- "when",
13298
- "questions"
13299
- ]
13300
- },
13301
- "explanation": { "type": "string" },
13302
- "assumptions": {
13303
- "type": "array",
13304
- "items": { "type": "string" }
13305
- },
13306
- "warnings": {
13307
- "type": "array",
13308
- "items": { "type": "string" }
13309
- },
13310
- "compiled_by": {
13311
- "type": "string",
13312
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
13313
- },
13314
- "vocabulary": { "type": "string" },
13315
- "created_at": {
13316
- "type": "string",
13317
- "format": "date-time"
13318
- },
13319
- "sql": {
13320
- "description": "Present only when include_sql was true.",
13321
- "type": "object",
13322
- "properties": {
13323
- "expression": {
13324
- "type": "string",
13325
- "description": "A parameterized boolean expression over one row per email."
13471
+ "english": { "type": "string" },
13472
+ "tree": {
13473
+ "type": "object",
13474
+ "properties": {
13475
+ "version": {
13476
+ "type": "number",
13477
+ "const": 1
13478
+ },
13479
+ "vocabulary": {
13480
+ "type": "string",
13481
+ "description": "Vocabulary version, such as v1."
13482
+ },
13483
+ "when": {
13484
+ "type": "object",
13485
+ "propertyNames": { "type": "string" },
13486
+ "additionalProperties": {},
13487
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
13488
+ },
13489
+ "questions": {
13490
+ "type": "array",
13491
+ "items": { "type": "string" },
13492
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
13493
+ }
13326
13494
  },
13327
- "params": {
13328
- "type": "array",
13329
- "items": {},
13330
- "description": "The values bound to $1, $2 and so on."
13331
- }
13495
+ "required": [
13496
+ "version",
13497
+ "vocabulary",
13498
+ "when",
13499
+ "questions"
13500
+ ]
13332
13501
  },
13333
- "required": ["expression", "params"]
13334
- }
13335
- },
13336
- "required": [
13337
- "version",
13338
- "english",
13339
- "tree",
13340
- "explanation",
13341
- "assumptions",
13342
- "warnings",
13343
- "compiled_by",
13344
- "vocabulary",
13345
- "created_at"
13346
- ]
13347
- }, { "type": "null" }] }
13502
+ "explanation": { "type": "string" },
13503
+ "assumptions": {
13504
+ "type": "array",
13505
+ "items": { "type": "string" }
13506
+ },
13507
+ "warnings": {
13508
+ "type": "array",
13509
+ "items": { "type": "string" }
13510
+ },
13511
+ "compiled_by": {
13512
+ "type": "string",
13513
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
13514
+ },
13515
+ "vocabulary": { "type": "string" },
13516
+ "created_at": {
13517
+ "type": "string",
13518
+ "format": "date-time"
13519
+ },
13520
+ "sql": {
13521
+ "description": "Present only when include_sql was true.",
13522
+ "type": "object",
13523
+ "properties": {
13524
+ "expression": {
13525
+ "type": "string",
13526
+ "description": "A parameterized boolean expression over one row per email."
13527
+ },
13528
+ "params": {
13529
+ "type": "array",
13530
+ "items": {},
13531
+ "description": "The values bound to $1, $2 and so on."
13532
+ }
13533
+ },
13534
+ "required": ["expression", "params"]
13535
+ }
13536
+ },
13537
+ "required": [
13538
+ "version",
13539
+ "english",
13540
+ "tree",
13541
+ "explanation",
13542
+ "assumptions",
13543
+ "warnings",
13544
+ "compiled_by",
13545
+ "vocabulary",
13546
+ "created_at"
13547
+ ]
13548
+ }, { "type": "null" }]
13549
+ }
13348
13550
  },
13349
13551
  "required": [
13350
13552
  "id",
@@ -13514,6 +13716,10 @@ const operationManifest = [
13514
13716
  "rule",
13515
13717
  "rule_id"
13516
13718
  ]
13719
+ },
13720
+ "notice": {
13721
+ "description": "What is now shared, in words.",
13722
+ "type": "string"
13517
13723
  }
13518
13724
  },
13519
13725
  "required": [
@@ -13613,7 +13819,7 @@ const operationManifest = [
13613
13819
  "binaryResponse": false,
13614
13820
  "bodyRequired": true,
13615
13821
  "command": "compile-email-rule",
13616
- "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), so a warning can say 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.",
13822
+ "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. 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.",
13617
13823
  "hasJsonBody": true,
13618
13824
  "method": "POST",
13619
13825
  "operationId": "compileEmailRule",
@@ -13686,7 +13892,7 @@ const operationManifest = [
13686
13892
  },
13687
13893
  "explanation": {
13688
13894
  "type": "string",
13689
- "description": "What the rule matches, in plain English."
13895
+ "description": "What the rule matches, in plain English: only which emails, not what it grants or shares, which follows from its state, its scope and the keys, webhooks and people it reaches. The compiler leaves out a sentence that plainly says what the rule hands over."
13690
13896
  },
13691
13897
  "assumptions": {
13692
13898
  "type": "array",
@@ -13696,12 +13902,31 @@ const operationManifest = [
13696
13902
  "warnings": {
13697
13903
  "type": "array",
13698
13904
  "items": { "type": "string" },
13699
- "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."
13905
+ "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."
13700
13906
  },
13701
13907
  "compiled_by": {
13702
13908
  "type": "string",
13703
13909
  "description": "An opaque name for the version of the rule compiler that produced the rule, such as rule-compiler-v1. Send it back as compiled_by when you save the rule."
13704
13910
  },
13911
+ "breadth": {
13912
+ "description": "How many of your recent emails its conditions hold for, counted over your own mailboxes whatever scope you save it with, warning or not. Null when not counted in full: a rule with questions, headers-only access, no test of it possible as you, or a count that failed or ran out of time. Writes ignore it, so a compile answer (without include_sql) can be sent back unchanged.",
13913
+ "anyOf": [{
13914
+ "type": "object",
13915
+ "properties": {
13916
+ "checked": {
13917
+ "type": "integer",
13918
+ "minimum": 0,
13919
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
13920
+ },
13921
+ "matched": {
13922
+ "type": "integer",
13923
+ "minimum": 0,
13924
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
13925
+ }
13926
+ },
13927
+ "required": ["checked", "matched"]
13928
+ }, { "type": "null" }]
13929
+ },
13705
13930
  "sql": {
13706
13931
  "description": "Present only when include_sql was true.",
13707
13932
  "type": "object",
@@ -13736,7 +13961,7 @@ const operationManifest = [
13736
13961
  "binaryResponse": false,
13737
13962
  "bodyRequired": true,
13738
13963
  "command": "create-email-rule",
13739
- "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.",
13964
+ "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.",
13740
13965
  "hasJsonBody": true,
13741
13966
  "method": "POST",
13742
13967
  "operationId": "createEmailRule",
@@ -13767,9 +13992,13 @@ const operationManifest = [
13767
13992
  "enum": ["grant", "do_not_share"]
13768
13993
  },
13769
13994
  "state": {
13770
- "description": "observe (the default) records what the rule would match and grants or blocks nothing; draft is not evaluated.",
13995
+ "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.",
13771
13996
  "type": "string",
13772
- "enum": ["draft", "observe"]
13997
+ "enum": [
13998
+ "draft",
13999
+ "observe",
14000
+ "active"
14001
+ ]
13773
14002
  },
13774
14003
  "evaluation_scope": {
13775
14004
  "type": "string",
@@ -13830,6 +14059,112 @@ const operationManifest = [
13830
14059
  "additionalProperties": false,
13831
14060
  "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."
13832
14061
  },
14062
+ "expectation_samples": {
14063
+ "description": "Made-up emails it must match or not (at most 20).",
14064
+ "minItems": 1,
14065
+ "maxItems": 20,
14066
+ "type": "array",
14067
+ "items": {
14068
+ "type": "object",
14069
+ "properties": {
14070
+ "expected": {
14071
+ "type": "string",
14072
+ "enum": ["match", "no_match"]
14073
+ },
14074
+ "label": {
14075
+ "type": ["string", "null"],
14076
+ "minLength": 1,
14077
+ "maxLength": 200,
14078
+ "pattern": "^[^\\u0000]*$",
14079
+ "description": "What the email stands for, such as a personal medical bill."
14080
+ },
14081
+ "email": {
14082
+ "type": "object",
14083
+ "properties": {
14084
+ "from": {
14085
+ "type": "object",
14086
+ "properties": {
14087
+ "address": {
14088
+ "type": "string",
14089
+ "maxLength": 254,
14090
+ "format": "email",
14091
+ "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,}$"
14092
+ },
14093
+ "name": {
14094
+ "type": "string",
14095
+ "maxLength": 200,
14096
+ "pattern": "^[^\\u0000]*$"
14097
+ }
14098
+ },
14099
+ "required": ["address"]
14100
+ },
14101
+ "to": {
14102
+ "minItems": 1,
14103
+ "maxItems": 10,
14104
+ "type": "array",
14105
+ "items": {
14106
+ "type": "string",
14107
+ "maxLength": 254,
14108
+ "format": "email",
14109
+ "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,}$"
14110
+ }
14111
+ },
14112
+ "cc": {
14113
+ "maxItems": 10,
14114
+ "type": "array",
14115
+ "items": {
14116
+ "type": "string",
14117
+ "maxLength": 254,
14118
+ "format": "email",
14119
+ "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,}$"
14120
+ }
14121
+ },
14122
+ "subject": {
14123
+ "type": "string",
14124
+ "maxLength": 998,
14125
+ "pattern": "^[^\\u0000]*$"
14126
+ },
14127
+ "text": {
14128
+ "type": "string",
14129
+ "minLength": 1,
14130
+ "maxLength": 4e3,
14131
+ "pattern": "^[^\\u0000]*$"
14132
+ },
14133
+ "attachment_types": {
14134
+ "maxItems": 10,
14135
+ "type": "array",
14136
+ "items": {
14137
+ "type": "string",
14138
+ "minLength": 1,
14139
+ "maxLength": 255,
14140
+ "pattern": "^[^\\u0000]*$"
14141
+ }
14142
+ },
14143
+ "automated": { "type": "boolean" },
14144
+ "from_mailing_list": { "type": "boolean" },
14145
+ "spam_score": {
14146
+ "type": "number",
14147
+ "minimum": -1e3,
14148
+ "maximum": 1e3
14149
+ }
14150
+ },
14151
+ "required": [
14152
+ "from",
14153
+ "to",
14154
+ "text"
14155
+ ],
14156
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
14157
+ },
14158
+ "id": {
14159
+ "type": "string",
14160
+ "format": "uuid"
14161
+ },
14162
+ "created_at": { "type": "string" }
14163
+ },
14164
+ "required": ["expected", "email"],
14165
+ "additionalProperties": false
14166
+ }
14167
+ },
13833
14168
  "english": {
13834
14169
  "type": "string",
13835
14170
  "minLength": 1,
@@ -13897,6 +14232,25 @@ const operationManifest = [
13897
14232
  "type": "string",
13898
14233
  "minLength": 1,
13899
14234
  "maxLength": 64
14235
+ },
14236
+ "breadth": {
14237
+ "description": "Ignored (the compile answer's breadth).",
14238
+ "anyOf": [{
14239
+ "type": "object",
14240
+ "properties": {
14241
+ "checked": {
14242
+ "type": "integer",
14243
+ "minimum": 0,
14244
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
14245
+ },
14246
+ "matched": {
14247
+ "type": "integer",
14248
+ "minimum": 0,
14249
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
14250
+ }
14251
+ },
14252
+ "required": ["checked", "matched"]
14253
+ }, { "type": "null" }]
13900
14254
  }
13901
14255
  },
13902
14256
  "required": [
@@ -13927,6 +14281,10 @@ const operationManifest = [
13927
14281
  "type": "string",
13928
14282
  "enum": ["grant", "do_not_share"]
13929
14283
  },
14284
+ "protection_note": {
14285
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), or a classifier fact narrows it.",
14286
+ "type": "string"
14287
+ },
13930
14288
  "evaluation_scope": {
13931
14289
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
13932
14290
  "type": "string",
@@ -13957,10 +14315,37 @@ const operationManifest = [
13957
14315
  "type": ["integer", "null"],
13958
14316
  "exclusiveMinimum": 0
13959
14317
  },
14318
+ "revision": {
14319
+ "description": "Moves on by one with every update. Send it as if_revision.",
14320
+ "type": "integer",
14321
+ "exclusiveMinimum": 0
14322
+ },
13960
14323
  "author_user_id": {
13961
14324
  "type": "string",
13962
14325
  "format": "uuid"
13963
14326
  },
14327
+ "author": {
14328
+ "type": "object",
14329
+ "properties": {
14330
+ "user_id": {
14331
+ "type": "string",
14332
+ "format": "uuid"
14333
+ },
14334
+ "name": {
14335
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
14336
+ "type": ["string", "null"]
14337
+ },
14338
+ "email": {
14339
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
14340
+ "type": ["string", "null"]
14341
+ }
14342
+ },
14343
+ "required": [
14344
+ "user_id",
14345
+ "name",
14346
+ "email"
14347
+ ]
14348
+ },
13964
14349
  "is_author": {
13965
14350
  "type": "boolean",
13966
14351
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -13973,91 +14358,94 @@ const operationManifest = [
13973
14358
  "type": "string",
13974
14359
  "format": "date-time"
13975
14360
  },
13976
- "current": { "anyOf": [{
13977
- "type": "object",
13978
- "properties": {
13979
- "version": {
13980
- "type": "integer",
13981
- "exclusiveMinimum": 0
13982
- },
13983
- "english": { "type": "string" },
13984
- "tree": {
13985
- "type": "object",
13986
- "properties": {
13987
- "version": {
13988
- "type": "number",
13989
- "const": 1
13990
- },
13991
- "vocabulary": {
13992
- "type": "string",
13993
- "description": "Vocabulary version, such as v1."
13994
- },
13995
- "when": {
13996
- "type": "object",
13997
- "propertyNames": { "type": "string" },
13998
- "additionalProperties": {},
13999
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
14000
- },
14001
- "questions": {
14002
- "type": "array",
14003
- "items": { "type": "string" },
14004
- "description": "Yes/no questions asked once the conditions match; all must be yes."
14005
- }
14361
+ "current": {
14362
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
14363
+ "anyOf": [{
14364
+ "type": "object",
14365
+ "properties": {
14366
+ "version": {
14367
+ "type": "integer",
14368
+ "exclusiveMinimum": 0
14006
14369
  },
14007
- "required": [
14008
- "version",
14009
- "vocabulary",
14010
- "when",
14011
- "questions"
14012
- ]
14013
- },
14014
- "explanation": { "type": "string" },
14015
- "assumptions": {
14016
- "type": "array",
14017
- "items": { "type": "string" }
14018
- },
14019
- "warnings": {
14020
- "type": "array",
14021
- "items": { "type": "string" }
14022
- },
14023
- "compiled_by": {
14024
- "type": "string",
14025
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
14026
- },
14027
- "vocabulary": { "type": "string" },
14028
- "created_at": {
14029
- "type": "string",
14030
- "format": "date-time"
14031
- },
14032
- "sql": {
14033
- "description": "Present only when include_sql was true.",
14034
- "type": "object",
14035
- "properties": {
14036
- "expression": {
14037
- "type": "string",
14038
- "description": "A parameterized boolean expression over one row per email."
14370
+ "english": { "type": "string" },
14371
+ "tree": {
14372
+ "type": "object",
14373
+ "properties": {
14374
+ "version": {
14375
+ "type": "number",
14376
+ "const": 1
14377
+ },
14378
+ "vocabulary": {
14379
+ "type": "string",
14380
+ "description": "Vocabulary version, such as v1."
14381
+ },
14382
+ "when": {
14383
+ "type": "object",
14384
+ "propertyNames": { "type": "string" },
14385
+ "additionalProperties": {},
14386
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
14387
+ },
14388
+ "questions": {
14389
+ "type": "array",
14390
+ "items": { "type": "string" },
14391
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
14392
+ }
14039
14393
  },
14040
- "params": {
14041
- "type": "array",
14042
- "items": {},
14043
- "description": "The values bound to $1, $2 and so on."
14044
- }
14394
+ "required": [
14395
+ "version",
14396
+ "vocabulary",
14397
+ "when",
14398
+ "questions"
14399
+ ]
14045
14400
  },
14046
- "required": ["expression", "params"]
14047
- }
14048
- },
14049
- "required": [
14050
- "version",
14051
- "english",
14052
- "tree",
14053
- "explanation",
14054
- "assumptions",
14055
- "warnings",
14056
- "compiled_by",
14057
- "vocabulary",
14058
- "created_at"
14059
- ]
14060
- }, { "type": "null" }] },
14401
+ "explanation": { "type": "string" },
14402
+ "assumptions": {
14403
+ "type": "array",
14404
+ "items": { "type": "string" }
14405
+ },
14406
+ "warnings": {
14407
+ "type": "array",
14408
+ "items": { "type": "string" }
14409
+ },
14410
+ "compiled_by": {
14411
+ "type": "string",
14412
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
14413
+ },
14414
+ "vocabulary": { "type": "string" },
14415
+ "created_at": {
14416
+ "type": "string",
14417
+ "format": "date-time"
14418
+ },
14419
+ "sql": {
14420
+ "description": "Present only when include_sql was true.",
14421
+ "type": "object",
14422
+ "properties": {
14423
+ "expression": {
14424
+ "type": "string",
14425
+ "description": "A parameterized boolean expression over one row per email."
14426
+ },
14427
+ "params": {
14428
+ "type": "array",
14429
+ "items": {},
14430
+ "description": "The values bound to $1, $2 and so on."
14431
+ }
14432
+ },
14433
+ "required": ["expression", "params"]
14434
+ }
14435
+ },
14436
+ "required": [
14437
+ "version",
14438
+ "english",
14439
+ "tree",
14440
+ "explanation",
14441
+ "assumptions",
14442
+ "warnings",
14443
+ "compiled_by",
14444
+ "vocabulary",
14445
+ "created_at"
14446
+ ]
14447
+ }, { "type": "null" }]
14448
+ },
14061
14449
  "scope_warnings": {
14062
14450
  "description": "Names in scope_selection that match nothing this organization receives mail at (a typo, or an inbox not yet in use). Present only when there are some.",
14063
14451
  "type": "array",
@@ -14088,8 +14476,114 @@ const operationManifest = [
14088
14476
  ]
14089
14477
  }
14090
14478
  },
14479
+ "expectation_samples": {
14480
+ "description": "Its saved made-up emails, when this write set or added some.",
14481
+ "type": "array",
14482
+ "items": {
14483
+ "type": "object",
14484
+ "properties": {
14485
+ "id": {
14486
+ "type": "string",
14487
+ "format": "uuid"
14488
+ },
14489
+ "expected": {
14490
+ "type": "string",
14491
+ "enum": ["match", "no_match"]
14492
+ },
14493
+ "label": { "type": ["string", "null"] },
14494
+ "email": {
14495
+ "type": "object",
14496
+ "properties": {
14497
+ "from": {
14498
+ "type": "object",
14499
+ "properties": {
14500
+ "address": {
14501
+ "type": "string",
14502
+ "maxLength": 254,
14503
+ "format": "email",
14504
+ "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,}$"
14505
+ },
14506
+ "name": {
14507
+ "type": "string",
14508
+ "maxLength": 200,
14509
+ "pattern": "^[^\\u0000]*$"
14510
+ }
14511
+ },
14512
+ "required": ["address"]
14513
+ },
14514
+ "to": {
14515
+ "minItems": 1,
14516
+ "maxItems": 10,
14517
+ "type": "array",
14518
+ "items": {
14519
+ "type": "string",
14520
+ "maxLength": 254,
14521
+ "format": "email",
14522
+ "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,}$"
14523
+ }
14524
+ },
14525
+ "cc": {
14526
+ "maxItems": 10,
14527
+ "type": "array",
14528
+ "items": {
14529
+ "type": "string",
14530
+ "maxLength": 254,
14531
+ "format": "email",
14532
+ "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,}$"
14533
+ }
14534
+ },
14535
+ "subject": {
14536
+ "type": "string",
14537
+ "maxLength": 998,
14538
+ "pattern": "^[^\\u0000]*$"
14539
+ },
14540
+ "text": {
14541
+ "type": "string",
14542
+ "minLength": 1,
14543
+ "maxLength": 4e3,
14544
+ "pattern": "^[^\\u0000]*$"
14545
+ },
14546
+ "attachment_types": {
14547
+ "maxItems": 10,
14548
+ "type": "array",
14549
+ "items": {
14550
+ "type": "string",
14551
+ "minLength": 1,
14552
+ "maxLength": 255,
14553
+ "pattern": "^[^\\u0000]*$"
14554
+ }
14555
+ },
14556
+ "automated": { "type": "boolean" },
14557
+ "from_mailing_list": { "type": "boolean" },
14558
+ "spam_score": {
14559
+ "type": "number",
14560
+ "minimum": -1e3,
14561
+ "maximum": 1e3
14562
+ }
14563
+ },
14564
+ "required": [
14565
+ "from",
14566
+ "to",
14567
+ "text"
14568
+ ],
14569
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
14570
+ },
14571
+ "created_at": {
14572
+ "type": "string",
14573
+ "format": "date-time"
14574
+ }
14575
+ },
14576
+ "required": [
14577
+ "id",
14578
+ "expected",
14579
+ "label",
14580
+ "email",
14581
+ "created_at"
14582
+ ]
14583
+ }
14584
+ },
14091
14585
  "expectations_check": {
14092
- "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.",
14586
+ "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.",
14093
14587
  "type": "object",
14094
14588
  "properties": {
14095
14589
  "version": {
@@ -14101,7 +14595,7 @@ const operationManifest = [
14101
14595
  "type": ["boolean", "null"]
14102
14596
  },
14103
14597
  "decided": {
14104
- "description": "Whether every expectation was judged (undecided is empty). Absent from a server that predates it.",
14598
+ "description": "Whether every expectation was judged (undecided and undecided_samples are empty). Absent from a server that predates it.",
14105
14599
  "type": "boolean"
14106
14600
  },
14107
14601
  "failed": {
@@ -14117,6 +14611,26 @@ const operationManifest = [
14117
14611
  "type": "string",
14118
14612
  "format": "uuid"
14119
14613
  }
14614
+ },
14615
+ "failed_samples": {
14616
+ "description": "Made-up emails that failed, by index in the set judged (saved oldest first, then added; or as sent).",
14617
+ "type": "array",
14618
+ "items": {
14619
+ "type": "integer",
14620
+ "minimum": 0
14621
+ }
14622
+ },
14623
+ "undecided_samples": {
14624
+ "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.",
14625
+ "type": "array",
14626
+ "items": {
14627
+ "type": "integer",
14628
+ "minimum": 0
14629
+ }
14630
+ },
14631
+ "note": {
14632
+ "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.",
14633
+ "type": "string"
14120
14634
  }
14121
14635
  },
14122
14636
  "required": [
@@ -14125,6 +14639,41 @@ const operationManifest = [
14125
14639
  "failed",
14126
14640
  "undecided"
14127
14641
  ]
14642
+ },
14643
+ "restored_from": {
14644
+ "description": "When this write saved an earlier version again (restore_version): which, and that the scope is not part of a version.",
14645
+ "type": "object",
14646
+ "properties": {
14647
+ "version": {
14648
+ "type": "integer",
14649
+ "exclusiveMinimum": 0
14650
+ },
14651
+ "note": { "type": "string" }
14652
+ },
14653
+ "required": ["version", "note"]
14654
+ },
14655
+ "affects": {
14656
+ "description": "After an update that saved a version: API keys listing the rule, the webhooks they own, and shares this write ended. The rule keeps its state. Absent if not read.",
14657
+ "type": "object",
14658
+ "properties": {
14659
+ "keys": {
14660
+ "type": "integer",
14661
+ "minimum": 0
14662
+ },
14663
+ "endpoints": {
14664
+ "type": "integer",
14665
+ "minimum": 0
14666
+ },
14667
+ "shares_ended": {
14668
+ "type": "integer",
14669
+ "minimum": 0
14670
+ }
14671
+ },
14672
+ "required": [
14673
+ "keys",
14674
+ "endpoints",
14675
+ "shares_ended"
14676
+ ]
14128
14677
  }
14129
14678
  },
14130
14679
  "required": [
@@ -14258,6 +14807,25 @@ const operationManifest = [
14258
14807
  "type": "string",
14259
14808
  "minLength": 1,
14260
14809
  "maxLength": 64
14810
+ },
14811
+ "breadth": {
14812
+ "description": "Ignored (the compile answer's breadth).",
14813
+ "anyOf": [{
14814
+ "type": "object",
14815
+ "properties": {
14816
+ "checked": {
14817
+ "type": "integer",
14818
+ "minimum": 0,
14819
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
14820
+ },
14821
+ "matched": {
14822
+ "type": "integer",
14823
+ "minimum": 0,
14824
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
14825
+ }
14826
+ },
14827
+ "required": ["checked", "matched"]
14828
+ }, { "type": "null" }]
14261
14829
  }
14262
14830
  },
14263
14831
  "required": [
@@ -14415,6 +14983,14 @@ const operationManifest = [
14415
14983
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14416
14984
  "type": ["string", "null"]
14417
14985
  },
14986
+ "edited_on_accept": {
14987
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
14988
+ "type": ["boolean", "null"]
14989
+ },
14990
+ "notice": {
14991
+ "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.",
14992
+ "type": ["string", "null"]
14993
+ },
14418
14994
  "target_can_accept": {
14419
14995
  "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.",
14420
14996
  "type": ["boolean", "null"]
@@ -14635,6 +15211,14 @@ const operationManifest = [
14635
15211
  "decline_reason": {
14636
15212
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14637
15213
  "type": ["string", "null"]
15214
+ },
15215
+ "edited_on_accept": {
15216
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
15217
+ "type": ["boolean", "null"]
15218
+ },
15219
+ "notice": {
15220
+ "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.",
15221
+ "type": ["string", "null"]
14638
15222
  }
14639
15223
  },
14640
15224
  "required": [
@@ -14792,11 +15376,325 @@ const operationManifest = [
14792
15376
  "tag": "Email Rules",
14793
15377
  "tagCommand": "email-rules"
14794
15378
  },
15379
+ {
15380
+ "binaryResponse": false,
15381
+ "bodyRequired": true,
15382
+ "command": "generate-look-alikes",
15383
+ "description": "Writes made-up emails that come close to what a grant rule describes but are private (a personal medical bill, pay for named people, a personal purchase, personal banking or tax, or another near case), and reports what the rule decides for each, as test-sample would. failed means the rule matched it, so it would be shared. Nothing is stored and no mail is read. Keep the ones it must never match 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.",
15384
+ "hasJsonBody": true,
15385
+ "method": "POST",
15386
+ "operationId": "generateLookAlikes",
15387
+ "path": "/email-rules/look-alikes",
15388
+ "pathParams": [],
15389
+ "queryParams": [],
15390
+ "headerParams": [{
15391
+ "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.",
15392
+ "enum": null,
15393
+ "name": "Idempotency-Key",
15394
+ "required": false,
15395
+ "type": "string"
15396
+ }],
15397
+ "permissions": ["mail:read"],
15398
+ "access": null,
15399
+ "requestSchema": {
15400
+ "type": "object",
15401
+ "properties": {
15402
+ "tree": {
15403
+ "type": "object",
15404
+ "properties": {
15405
+ "version": {
15406
+ "type": "number",
15407
+ "const": 1
15408
+ },
15409
+ "vocabulary": {
15410
+ "type": "string",
15411
+ "description": "Vocabulary version, such as v1."
15412
+ },
15413
+ "when": {
15414
+ "type": "object",
15415
+ "propertyNames": { "type": "string" },
15416
+ "additionalProperties": {},
15417
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
15418
+ },
15419
+ "questions": {
15420
+ "description": "Yes/no questions asked once the conditions match; all must be yes. Left out, none.",
15421
+ "type": "array",
15422
+ "items": { "type": "string" }
15423
+ }
15424
+ },
15425
+ "required": [
15426
+ "version",
15427
+ "vocabulary",
15428
+ "when"
15429
+ ],
15430
+ "additionalProperties": false
15431
+ },
15432
+ "english": {
15433
+ "description": "What the tree is meant to match; without it, the tree is described in words.",
15434
+ "type": "string",
15435
+ "minLength": 1,
15436
+ "maxLength": 2e3
15437
+ },
15438
+ "evaluation_scope": {
15439
+ "type": "string",
15440
+ "enum": [
15441
+ "author",
15442
+ "own_mailboxes",
15443
+ "selected"
15444
+ ],
15445
+ "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."
15446
+ },
15447
+ "scope_selection": {
15448
+ "type": "object",
15449
+ "properties": {
15450
+ "addresses": {
15451
+ "maxItems": 100,
15452
+ "type": "array",
15453
+ "items": {
15454
+ "type": "string",
15455
+ "minLength": 3,
15456
+ "maxLength": 320,
15457
+ "pattern": "^[^\\s@]+@[^\\s@]+$"
15458
+ }
15459
+ },
15460
+ "domains": {
15461
+ "maxItems": 50,
15462
+ "type": "array",
15463
+ "items": {
15464
+ "type": "string",
15465
+ "minLength": 1,
15466
+ "maxLength": 253,
15467
+ "pattern": "^[^\\s@]+\\.[^\\s@]+$"
15468
+ }
15469
+ }
15470
+ },
15471
+ "additionalProperties": false,
15472
+ "description": "For a selected rule: inboxes, and domains (each address at them the author reads); each one you can read."
15473
+ },
15474
+ "rule_id": {
15475
+ "description": "A saved rule you can see: its current version and words.",
15476
+ "type": "string",
15477
+ "format": "uuid"
15478
+ },
15479
+ "request_id": {
15480
+ "description": "A rule request you can see: its rule as asked.",
15481
+ "type": "string",
15482
+ "format": "uuid"
15483
+ },
15484
+ "kind": {
15485
+ "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.",
15486
+ "type": "string",
15487
+ "enum": ["grant", "do_not_share"]
15488
+ },
15489
+ "count": {
15490
+ "description": "How many, 1 to 8; 6 by default.",
15491
+ "type": "integer",
15492
+ "minimum": 1,
15493
+ "maximum": 8
15494
+ },
15495
+ "kinds": {
15496
+ "description": "Which kinds, taken in turn; all by default, the private ones first.",
15497
+ "minItems": 1,
15498
+ "maxItems": 5,
15499
+ "type": "array",
15500
+ "items": {
15501
+ "type": "string",
15502
+ "enum": [
15503
+ "medical",
15504
+ "payroll",
15505
+ "personal_purchase",
15506
+ "bank_tax",
15507
+ "other"
15508
+ ]
15509
+ }
15510
+ },
15511
+ "to": {
15512
+ "description": "The mailbox they arrive in; by default the rule author's (a request's target's) sign-in address when you may see it, else yours.",
15513
+ "type": "string",
15514
+ "maxLength": 254,
15515
+ "format": "email",
15516
+ "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,}$"
15517
+ }
15518
+ },
15519
+ "additionalProperties": false
15520
+ },
15521
+ "responseSchema": {
15522
+ "type": "object",
15523
+ "properties": {
15524
+ "rule": {
15525
+ "type": "object",
15526
+ "properties": {
15527
+ "id": {
15528
+ "type": ["string", "null"],
15529
+ "format": "uuid"
15530
+ },
15531
+ "version": {
15532
+ "type": ["integer", "null"],
15533
+ "exclusiveMinimum": 0
15534
+ },
15535
+ "vocabulary": { "type": "string" }
15536
+ },
15537
+ "required": [
15538
+ "id",
15539
+ "version",
15540
+ "vocabulary"
15541
+ ]
15542
+ },
15543
+ "look_alikes": {
15544
+ "type": "array",
15545
+ "items": {
15546
+ "type": "object",
15547
+ "properties": {
15548
+ "kind": {
15549
+ "type": "string",
15550
+ "enum": [
15551
+ "medical",
15552
+ "payroll",
15553
+ "personal_purchase",
15554
+ "bank_tax",
15555
+ "other"
15556
+ ]
15557
+ },
15558
+ "why": {
15559
+ "type": "string",
15560
+ "description": "Why the rule should not match it."
15561
+ },
15562
+ "email": {
15563
+ "type": "object",
15564
+ "properties": {
15565
+ "from": {
15566
+ "type": "object",
15567
+ "properties": {
15568
+ "address": { "type": "string" },
15569
+ "name": { "type": "string" }
15570
+ },
15571
+ "required": ["address"]
15572
+ },
15573
+ "to": {
15574
+ "type": "array",
15575
+ "items": { "type": "string" }
15576
+ },
15577
+ "cc": {
15578
+ "type": "array",
15579
+ "items": { "type": "string" }
15580
+ },
15581
+ "subject": { "type": "string" },
15582
+ "text": { "type": "string" },
15583
+ "attachment_types": {
15584
+ "type": "array",
15585
+ "items": { "type": "string" }
15586
+ },
15587
+ "automated": { "type": "boolean" },
15588
+ "from_mailing_list": { "type": "boolean" }
15589
+ },
15590
+ "required": [
15591
+ "from",
15592
+ "to",
15593
+ "cc",
15594
+ "subject",
15595
+ "text",
15596
+ "attachment_types",
15597
+ "automated",
15598
+ "from_mailing_list"
15599
+ ],
15600
+ "description": "A made-up email, as test-sample takes it."
15601
+ },
15602
+ "outcome": {
15603
+ "type": "string",
15604
+ "enum": [
15605
+ "matched",
15606
+ "near_miss",
15607
+ "no_match",
15608
+ "not_decided"
15609
+ ]
15610
+ },
15611
+ "status": {
15612
+ "type": "string",
15613
+ "enum": [
15614
+ "passed",
15615
+ "failed",
15616
+ "undecided"
15617
+ ],
15618
+ "description": "failed: the rule matched it, so it would be shared. 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)."
15619
+ },
15620
+ "question_answers": {
15621
+ "type": "array",
15622
+ "items": {
15623
+ "type": "object",
15624
+ "properties": {
15625
+ "question": { "type": "string" },
15626
+ "answer": { "type": ["boolean", "null"] },
15627
+ "probability": { "type": ["number", "null"] },
15628
+ "error_class": { "type": "string" }
15629
+ },
15630
+ "required": [
15631
+ "question",
15632
+ "answer",
15633
+ "probability"
15634
+ ]
15635
+ }
15636
+ }
15637
+ },
15638
+ "required": [
15639
+ "kind",
15640
+ "why",
15641
+ "email",
15642
+ "outcome",
15643
+ "status",
15644
+ "question_answers"
15645
+ ]
15646
+ }
15647
+ },
15648
+ "counts": {
15649
+ "type": "object",
15650
+ "properties": {
15651
+ "passed": {
15652
+ "type": "integer",
15653
+ "minimum": 0
15654
+ },
15655
+ "failed": {
15656
+ "type": "integer",
15657
+ "minimum": 0
15658
+ },
15659
+ "undecided": {
15660
+ "type": "integer",
15661
+ "minimum": 0
15662
+ },
15663
+ "dropped": {
15664
+ "type": "integer",
15665
+ "minimum": 0,
15666
+ "description": "Asked for but not returned: not written, outside the sample bounds, or of a kind not asked for."
15667
+ }
15668
+ },
15669
+ "required": [
15670
+ "passed",
15671
+ "failed",
15672
+ "undecided",
15673
+ "dropped"
15674
+ ]
15675
+ },
15676
+ "note": {
15677
+ "type": "string",
15678
+ "description": "That the emails are made up and were never sent or stored."
15679
+ }
15680
+ },
15681
+ "required": [
15682
+ "rule",
15683
+ "look_alikes",
15684
+ "counts",
15685
+ "note"
15686
+ ]
15687
+ },
15688
+ "sdkName": "generateLookAlikes",
15689
+ "summary": "Write look-alike emails a rule should not match",
15690
+ "tag": "Email Rules",
15691
+ "tagCommand": "email-rules"
15692
+ },
14795
15693
  {
14796
15694
  "binaryResponse": false,
14797
15695
  "bodyRequired": false,
14798
15696
  "command": "get-email-rule",
14799
- "description": "A rule, its scope (evaluation_scope, and scope_selection for a selected rule), its current version and its saved expectations. What the scope comes to for its author now is GET /email-rules/{id}/scope. The SQL the version renders to is included only when include_sql is true. 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.",
15697
+ "description": "A rule, its scope (evaluation_scope, and scope_selection for a selected rule), its current version and its saved expectations. The current version's text (english, tree, explanation, assumptions, warnings) is under current, and current_version is its number. What the scope comes to for its author now is GET /email-rules/{id}/scope. The SQL the version renders to is included only when include_sql is true. 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.",
14800
15698
  "hasJsonBody": false,
14801
15699
  "method": "GET",
14802
15700
  "operationId": "getEmailRule",
@@ -14840,6 +15738,10 @@ const operationManifest = [
14840
15738
  "type": "string",
14841
15739
  "enum": ["grant", "do_not_share"]
14842
15740
  },
15741
+ "protection_note": {
15742
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), or a classifier fact narrows it.",
15743
+ "type": "string"
15744
+ },
14843
15745
  "evaluation_scope": {
14844
15746
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
14845
15747
  "type": "string",
@@ -14870,10 +15772,37 @@ const operationManifest = [
14870
15772
  "type": ["integer", "null"],
14871
15773
  "exclusiveMinimum": 0
14872
15774
  },
15775
+ "revision": {
15776
+ "description": "Moves on by one with every update. Send it as if_revision.",
15777
+ "type": "integer",
15778
+ "exclusiveMinimum": 0
15779
+ },
14873
15780
  "author_user_id": {
14874
15781
  "type": "string",
14875
15782
  "format": "uuid"
14876
15783
  },
15784
+ "author": {
15785
+ "type": "object",
15786
+ "properties": {
15787
+ "user_id": {
15788
+ "type": "string",
15789
+ "format": "uuid"
15790
+ },
15791
+ "name": {
15792
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
15793
+ "type": ["string", "null"]
15794
+ },
15795
+ "email": {
15796
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
15797
+ "type": ["string", "null"]
15798
+ }
15799
+ },
15800
+ "required": [
15801
+ "user_id",
15802
+ "name",
15803
+ "email"
15804
+ ]
15805
+ },
14877
15806
  "is_author": {
14878
15807
  "type": "boolean",
14879
15808
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -14886,91 +15815,94 @@ const operationManifest = [
14886
15815
  "type": "string",
14887
15816
  "format": "date-time"
14888
15817
  },
14889
- "current": { "anyOf": [{
14890
- "type": "object",
14891
- "properties": {
14892
- "version": {
14893
- "type": "integer",
14894
- "exclusiveMinimum": 0
14895
- },
14896
- "english": { "type": "string" },
14897
- "tree": {
14898
- "type": "object",
14899
- "properties": {
14900
- "version": {
14901
- "type": "number",
14902
- "const": 1
14903
- },
14904
- "vocabulary": {
14905
- "type": "string",
14906
- "description": "Vocabulary version, such as v1."
14907
- },
14908
- "when": {
14909
- "type": "object",
14910
- "propertyNames": { "type": "string" },
14911
- "additionalProperties": {},
14912
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
14913
- },
14914
- "questions": {
14915
- "type": "array",
14916
- "items": { "type": "string" },
14917
- "description": "Yes/no questions asked once the conditions match; all must be yes."
14918
- }
15818
+ "current": {
15819
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
15820
+ "anyOf": [{
15821
+ "type": "object",
15822
+ "properties": {
15823
+ "version": {
15824
+ "type": "integer",
15825
+ "exclusiveMinimum": 0
14919
15826
  },
14920
- "required": [
14921
- "version",
14922
- "vocabulary",
14923
- "when",
14924
- "questions"
14925
- ]
14926
- },
14927
- "explanation": { "type": "string" },
14928
- "assumptions": {
14929
- "type": "array",
14930
- "items": { "type": "string" }
14931
- },
14932
- "warnings": {
14933
- "type": "array",
14934
- "items": { "type": "string" }
14935
- },
14936
- "compiled_by": {
14937
- "type": "string",
14938
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
14939
- },
14940
- "vocabulary": { "type": "string" },
14941
- "created_at": {
14942
- "type": "string",
14943
- "format": "date-time"
14944
- },
14945
- "sql": {
14946
- "description": "Present only when include_sql was true.",
14947
- "type": "object",
14948
- "properties": {
14949
- "expression": {
14950
- "type": "string",
14951
- "description": "A parameterized boolean expression over one row per email."
15827
+ "english": { "type": "string" },
15828
+ "tree": {
15829
+ "type": "object",
15830
+ "properties": {
15831
+ "version": {
15832
+ "type": "number",
15833
+ "const": 1
15834
+ },
15835
+ "vocabulary": {
15836
+ "type": "string",
15837
+ "description": "Vocabulary version, such as v1."
15838
+ },
15839
+ "when": {
15840
+ "type": "object",
15841
+ "propertyNames": { "type": "string" },
15842
+ "additionalProperties": {},
15843
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
15844
+ },
15845
+ "questions": {
15846
+ "type": "array",
15847
+ "items": { "type": "string" },
15848
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
15849
+ }
14952
15850
  },
14953
- "params": {
14954
- "type": "array",
14955
- "items": {},
14956
- "description": "The values bound to $1, $2 and so on."
14957
- }
15851
+ "required": [
15852
+ "version",
15853
+ "vocabulary",
15854
+ "when",
15855
+ "questions"
15856
+ ]
14958
15857
  },
14959
- "required": ["expression", "params"]
14960
- }
14961
- },
14962
- "required": [
14963
- "version",
14964
- "english",
14965
- "tree",
14966
- "explanation",
14967
- "assumptions",
14968
- "warnings",
14969
- "compiled_by",
14970
- "vocabulary",
14971
- "created_at"
14972
- ]
14973
- }, { "type": "null" }] },
15858
+ "explanation": { "type": "string" },
15859
+ "assumptions": {
15860
+ "type": "array",
15861
+ "items": { "type": "string" }
15862
+ },
15863
+ "warnings": {
15864
+ "type": "array",
15865
+ "items": { "type": "string" }
15866
+ },
15867
+ "compiled_by": {
15868
+ "type": "string",
15869
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
15870
+ },
15871
+ "vocabulary": { "type": "string" },
15872
+ "created_at": {
15873
+ "type": "string",
15874
+ "format": "date-time"
15875
+ },
15876
+ "sql": {
15877
+ "description": "Present only when include_sql was true.",
15878
+ "type": "object",
15879
+ "properties": {
15880
+ "expression": {
15881
+ "type": "string",
15882
+ "description": "A parameterized boolean expression over one row per email."
15883
+ },
15884
+ "params": {
15885
+ "type": "array",
15886
+ "items": {},
15887
+ "description": "The values bound to $1, $2 and so on."
15888
+ }
15889
+ },
15890
+ "required": ["expression", "params"]
15891
+ }
15892
+ },
15893
+ "required": [
15894
+ "version",
15895
+ "english",
15896
+ "tree",
15897
+ "explanation",
15898
+ "assumptions",
15899
+ "warnings",
15900
+ "compiled_by",
15901
+ "vocabulary",
15902
+ "created_at"
15903
+ ]
15904
+ }, { "type": "null" }]
15905
+ },
14974
15906
  "expectations": {
14975
15907
  "description": "Its saved expectations, each list by email id.",
14976
15908
  "type": "object",
@@ -14991,6 +15923,112 @@ const operationManifest = [
14991
15923
  }
14992
15924
  },
14993
15925
  "required": ["match", "no_match"]
15926
+ },
15927
+ "expectation_samples": {
15928
+ "description": "Its saved made-up emails, oldest first.",
15929
+ "type": "array",
15930
+ "items": {
15931
+ "type": "object",
15932
+ "properties": {
15933
+ "id": {
15934
+ "type": "string",
15935
+ "format": "uuid"
15936
+ },
15937
+ "expected": {
15938
+ "type": "string",
15939
+ "enum": ["match", "no_match"]
15940
+ },
15941
+ "label": { "type": ["string", "null"] },
15942
+ "email": {
15943
+ "type": "object",
15944
+ "properties": {
15945
+ "from": {
15946
+ "type": "object",
15947
+ "properties": {
15948
+ "address": {
15949
+ "type": "string",
15950
+ "maxLength": 254,
15951
+ "format": "email",
15952
+ "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,}$"
15953
+ },
15954
+ "name": {
15955
+ "type": "string",
15956
+ "maxLength": 200,
15957
+ "pattern": "^[^\\u0000]*$"
15958
+ }
15959
+ },
15960
+ "required": ["address"]
15961
+ },
15962
+ "to": {
15963
+ "minItems": 1,
15964
+ "maxItems": 10,
15965
+ "type": "array",
15966
+ "items": {
15967
+ "type": "string",
15968
+ "maxLength": 254,
15969
+ "format": "email",
15970
+ "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,}$"
15971
+ }
15972
+ },
15973
+ "cc": {
15974
+ "maxItems": 10,
15975
+ "type": "array",
15976
+ "items": {
15977
+ "type": "string",
15978
+ "maxLength": 254,
15979
+ "format": "email",
15980
+ "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,}$"
15981
+ }
15982
+ },
15983
+ "subject": {
15984
+ "type": "string",
15985
+ "maxLength": 998,
15986
+ "pattern": "^[^\\u0000]*$"
15987
+ },
15988
+ "text": {
15989
+ "type": "string",
15990
+ "minLength": 1,
15991
+ "maxLength": 4e3,
15992
+ "pattern": "^[^\\u0000]*$"
15993
+ },
15994
+ "attachment_types": {
15995
+ "maxItems": 10,
15996
+ "type": "array",
15997
+ "items": {
15998
+ "type": "string",
15999
+ "minLength": 1,
16000
+ "maxLength": 255,
16001
+ "pattern": "^[^\\u0000]*$"
16002
+ }
16003
+ },
16004
+ "automated": { "type": "boolean" },
16005
+ "from_mailing_list": { "type": "boolean" },
16006
+ "spam_score": {
16007
+ "type": "number",
16008
+ "minimum": -1e3,
16009
+ "maximum": 1e3
16010
+ }
16011
+ },
16012
+ "required": [
16013
+ "from",
16014
+ "to",
16015
+ "text"
16016
+ ],
16017
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
16018
+ },
16019
+ "created_at": {
16020
+ "type": "string",
16021
+ "format": "date-time"
16022
+ }
16023
+ },
16024
+ "required": [
16025
+ "id",
16026
+ "expected",
16027
+ "label",
16028
+ "email",
16029
+ "created_at"
16030
+ ]
16031
+ }
14994
16032
  }
14995
16033
  },
14996
16034
  "required": [
@@ -15014,7 +16052,7 @@ const operationManifest = [
15014
16052
  "binaryResponse": false,
15015
16053
  "bodyRequired": false,
15016
16054
  "command": "get-email-rule-request",
15017
- "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.",
16055
+ "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.",
15018
16056
  "hasJsonBody": false,
15019
16057
  "method": "GET",
15020
16058
  "operationId": "getEmailRuleRequest",
@@ -15178,6 +16216,14 @@ const operationManifest = [
15178
16216
  "decline_reason": {
15179
16217
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
15180
16218
  "type": ["string", "null"]
16219
+ },
16220
+ "edited_on_accept": {
16221
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
16222
+ "type": ["boolean", "null"]
16223
+ },
16224
+ "notice": {
16225
+ "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.",
16226
+ "type": ["string", "null"]
15181
16227
  }
15182
16228
  },
15183
16229
  "required": [
@@ -16060,6 +17106,14 @@ const operationManifest = [
16060
17106
  "decline_reason": {
16061
17107
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
16062
17108
  "type": ["string", "null"]
17109
+ },
17110
+ "edited_on_accept": {
17111
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
17112
+ "type": ["boolean", "null"]
17113
+ },
17114
+ "notice": {
17115
+ "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.",
17116
+ "type": ["string", "null"]
16063
17117
  }
16064
17118
  },
16065
17119
  "required": [
@@ -17144,7 +18198,7 @@ const operationManifest = [
17144
18198
  "binaryResponse": false,
17145
18199
  "bodyRequired": false,
17146
18200
  "command": "list-email-rules",
17147
- "description": "The rules the caller can see, newest first: their own, or every member's rules for a caller who reads the whole organization's mail. Each carries its scope (evaluation_scope, and scope_selection for a selected rule). With matched_email_id, only the rules that matched that 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.",
18201
+ "description": "The rules the caller can see, newest first: their own, or every member's rules for a caller who reads the whole organization's mail. Each carries its scope (evaluation_scope, and scope_selection for a selected rule) and current_version, but not the version's text: read that under current from GET /email-rules/{id}. With matched_email_id, only the rules that matched that 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.",
17148
18202
  "hasJsonBody": false,
17149
18203
  "method": "GET",
17150
18204
  "operationId": "listEmailRules",
@@ -17203,6 +18257,10 @@ const operationManifest = [
17203
18257
  "type": "string",
17204
18258
  "enum": ["grant", "do_not_share"]
17205
18259
  },
18260
+ "protection_note": {
18261
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), or a classifier fact narrows it.",
18262
+ "type": "string"
18263
+ },
17206
18264
  "evaluation_scope": {
17207
18265
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
17208
18266
  "type": "string",
@@ -17233,10 +18291,37 @@ const operationManifest = [
17233
18291
  "type": ["integer", "null"],
17234
18292
  "exclusiveMinimum": 0
17235
18293
  },
18294
+ "revision": {
18295
+ "description": "Moves on by one with every update. Send it as if_revision.",
18296
+ "type": "integer",
18297
+ "exclusiveMinimum": 0
18298
+ },
17236
18299
  "author_user_id": {
17237
18300
  "type": "string",
17238
18301
  "format": "uuid"
17239
18302
  },
18303
+ "author": {
18304
+ "type": "object",
18305
+ "properties": {
18306
+ "user_id": {
18307
+ "type": "string",
18308
+ "format": "uuid"
18309
+ },
18310
+ "name": {
18311
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
18312
+ "type": ["string", "null"]
18313
+ },
18314
+ "email": {
18315
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
18316
+ "type": ["string", "null"]
18317
+ }
18318
+ },
18319
+ "required": [
18320
+ "user_id",
18321
+ "name",
18322
+ "email"
18323
+ ]
18324
+ },
17240
18325
  "is_author": {
17241
18326
  "type": "boolean",
17242
18327
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -17608,10 +18693,114 @@ const operationManifest = [
17608
18693
  "type": "string",
17609
18694
  "format": "uuid"
17610
18695
  }
18696
+ },
18697
+ "samples": {
18698
+ "maxItems": 20,
18699
+ "type": "array",
18700
+ "items": {
18701
+ "type": "object",
18702
+ "properties": {
18703
+ "expected": {
18704
+ "type": "string",
18705
+ "enum": ["match", "no_match"]
18706
+ },
18707
+ "label": {
18708
+ "type": ["string", "null"],
18709
+ "minLength": 1,
18710
+ "maxLength": 200,
18711
+ "pattern": "^[^\\u0000]*$",
18712
+ "description": "What the email stands for, such as a personal medical bill."
18713
+ },
18714
+ "email": {
18715
+ "type": "object",
18716
+ "properties": {
18717
+ "from": {
18718
+ "type": "object",
18719
+ "properties": {
18720
+ "address": {
18721
+ "type": "string",
18722
+ "maxLength": 254,
18723
+ "format": "email",
18724
+ "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,}$"
18725
+ },
18726
+ "name": {
18727
+ "type": "string",
18728
+ "maxLength": 200,
18729
+ "pattern": "^[^\\u0000]*$"
18730
+ }
18731
+ },
18732
+ "required": ["address"]
18733
+ },
18734
+ "to": {
18735
+ "minItems": 1,
18736
+ "maxItems": 10,
18737
+ "type": "array",
18738
+ "items": {
18739
+ "type": "string",
18740
+ "maxLength": 254,
18741
+ "format": "email",
18742
+ "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,}$"
18743
+ }
18744
+ },
18745
+ "cc": {
18746
+ "maxItems": 10,
18747
+ "type": "array",
18748
+ "items": {
18749
+ "type": "string",
18750
+ "maxLength": 254,
18751
+ "format": "email",
18752
+ "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,}$"
18753
+ }
18754
+ },
18755
+ "subject": {
18756
+ "type": "string",
18757
+ "maxLength": 998,
18758
+ "pattern": "^[^\\u0000]*$"
18759
+ },
18760
+ "text": {
18761
+ "type": "string",
18762
+ "minLength": 1,
18763
+ "maxLength": 4e3,
18764
+ "pattern": "^[^\\u0000]*$"
18765
+ },
18766
+ "attachment_types": {
18767
+ "maxItems": 10,
18768
+ "type": "array",
18769
+ "items": {
18770
+ "type": "string",
18771
+ "minLength": 1,
18772
+ "maxLength": 255,
18773
+ "pattern": "^[^\\u0000]*$"
18774
+ }
18775
+ },
18776
+ "automated": { "type": "boolean" },
18777
+ "from_mailing_list": { "type": "boolean" },
18778
+ "spam_score": {
18779
+ "type": "number",
18780
+ "minimum": -1e3,
18781
+ "maximum": 1e3
18782
+ }
18783
+ },
18784
+ "required": [
18785
+ "from",
18786
+ "to",
18787
+ "text"
18788
+ ],
18789
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
18790
+ },
18791
+ "id": {
18792
+ "type": "string",
18793
+ "format": "uuid"
18794
+ },
18795
+ "created_at": { "type": "string" }
18796
+ },
18797
+ "required": ["expected", "email"],
18798
+ "additionalProperties": false
18799
+ }
17611
18800
  }
17612
18801
  },
17613
18802
  "additionalProperties": false,
17614
- "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."
18803
+ "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."
17615
18804
  }
17616
18805
  },
17617
18806
  "additionalProperties": false
@@ -17993,7 +19182,7 @@ const operationManifest = [
17993
19182
  }
17994
19183
  },
17995
19184
  "expectations": {
17996
- "description": "Present when expect was given, or for a saved rule with saved expectations.",
19185
+ "description": "Present when expect was given, or for a saved rule with saved expectations or made-up emails.",
17997
19186
  "type": "object",
17998
19187
  "properties": {
17999
19188
  "passed": {
@@ -18063,6 +19252,71 @@ const operationManifest = [
18063
19252
  ]
18064
19253
  },
18065
19254
  "description": "match first, then no_match, each in the order given (saved expectations by email id)."
19255
+ },
19256
+ "samples": {
19257
+ "description": "Made-up emails judged (expect.samples, or a saved rule's), in order; counts include them.",
19258
+ "type": "array",
19259
+ "items": {
19260
+ "type": "object",
19261
+ "properties": {
19262
+ "index": {
19263
+ "type": "integer",
19264
+ "minimum": 0
19265
+ },
19266
+ "sample_id": {
19267
+ "type": ["string", "null"],
19268
+ "format": "uuid"
19269
+ },
19270
+ "label": { "type": ["string", "null"] },
19271
+ "expected": {
19272
+ "type": "string",
19273
+ "enum": ["match", "no_match"]
19274
+ },
19275
+ "outcome": {
19276
+ "type": "string",
19277
+ "enum": [
19278
+ "matched",
19279
+ "near_miss",
19280
+ "no_match",
19281
+ "not_decided"
19282
+ ]
19283
+ },
19284
+ "status": {
19285
+ "type": "string",
19286
+ "enum": [
19287
+ "passed",
19288
+ "failed",
19289
+ "undecided"
19290
+ ]
19291
+ },
19292
+ "question_answers": {
19293
+ "type": "array",
19294
+ "items": {
19295
+ "type": "object",
19296
+ "properties": {
19297
+ "question": { "type": "string" },
19298
+ "answer": { "type": ["boolean", "null"] },
19299
+ "probability": { "type": ["number", "null"] },
19300
+ "error_class": { "type": "string" }
19301
+ },
19302
+ "required": [
19303
+ "question",
19304
+ "answer",
19305
+ "probability"
19306
+ ]
19307
+ }
19308
+ }
19309
+ },
19310
+ "required": [
19311
+ "index",
19312
+ "sample_id",
19313
+ "label",
19314
+ "expected",
19315
+ "outcome",
19316
+ "status",
19317
+ "question_answers"
19318
+ ]
19319
+ }
18066
19320
  }
18067
19321
  },
18068
19322
  "required": [
@@ -18437,7 +19691,7 @@ const operationManifest = [
18437
19691
  "binaryResponse": false,
18438
19692
  "bodyRequired": true,
18439
19693
  "command": "update-email-rule",
18440
- "description": "Renames the rule, saves a new version (english and tree together, with an explanation or one the server writes; earlier versions stay), changes its state (draft is off; observe; active), whose mail it runs over (evaluation_scope, with scope_selection when selected) or its saved expectations (each saved version and new set is judged in expectations_check). Anyone but its author who sees it may only disable it (state draft), and while it is shared only if they manage members from the dashboard. Without evaluation_scope the scope stays; a scope change saves a new version with the same conditions, and what was granted stays. A shared rule keeps its scope; a kind never changes. Send if_version, the current_version you read, so a write never replaces a version saved since. 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.",
19694
+ "description": "Renames the rule, saves a new version (english and tree together, with an explanation or one the server writes; earlier versions stay) or an earlier one again (restore_version), changes its state (draft is off; observe; active), whose mail it runs over (evaluation_scope, with scope_selection when selected) or its saved expectations (each saved version and new set is judged in expectations_check). Anyone but its author who sees it may only disable it (state draft), and while it is shared only if they manage members from the dashboard. Without evaluation_scope the scope stays; a scope change saves a new version with the same conditions, and what was granted stays. A shared rule keeps its scope; a kind never changes. A saved version takes effect at once (an active rule stays active); affects counts what it reaches. Send if_version, the current_version you read, so a write never replaces a version saved since, or if_revision to refuse it after any update. Without if_expectations_pass a version is saved even when its expectations_check fails. 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.",
18441
19695
  "hasJsonBody": true,
18442
19696
  "method": "PATCH",
18443
19697
  "operationId": "updateEmailRule",
@@ -18539,6 +19793,25 @@ const operationManifest = [
18539
19793
  "minLength": 1,
18540
19794
  "maxLength": 64
18541
19795
  },
19796
+ "breadth": {
19797
+ "description": "Ignored (the compile answer's breadth).",
19798
+ "anyOf": [{
19799
+ "type": "object",
19800
+ "properties": {
19801
+ "checked": {
19802
+ "type": "integer",
19803
+ "minimum": 0,
19804
+ "description": "Classified emails the conditions were decided for: of the newest 200 from the last 30 days in your own mailboxes (for a grant rule, those you may share), the ones you can still read."
19805
+ },
19806
+ "matched": {
19807
+ "type": "integer",
19808
+ "minimum": 0,
19809
+ "description": "Of those, how many they hold for (for a do-not-share rule, one they cannot decide counts)."
19810
+ }
19811
+ },
19812
+ "required": ["checked", "matched"]
19813
+ }, { "type": "null" }]
19814
+ },
18542
19815
  "evaluation_scope": {
18543
19816
  "type": "string",
18544
19817
  "enum": [
@@ -18576,7 +19849,7 @@ const operationManifest = [
18576
19849
  "description": "For a selected rule: inboxes, and domains (each address at them the author reads); each one you can read."
18577
19850
  },
18578
19851
  "if_version": {
18579
- "description": "The current_version you read. If the rule's now differs, nothing changes: 409 email_rule_conflict, details.reason version_moved and details.current_version. Send it with a new version so a concurrent edit is never replaced unseen. It guards versions only: a change to expectations alone saves none.",
19852
+ "description": "The current_version you read: if it has moved, nothing is saved (409 email_rule_conflict, version_moved). It guards versions only; if_revision guards every update.",
18580
19853
  "type": "integer",
18581
19854
  "minimum": 1
18582
19855
  },
@@ -18602,6 +19875,233 @@ const operationManifest = [
18602
19875
  },
18603
19876
  "additionalProperties": false,
18604
19877
  "description": "Replaces its saved expectations (author only); null removes them."
19878
+ },
19879
+ "expectation_samples": {
19880
+ "maxItems": 20,
19881
+ "type": ["array", "null"],
19882
+ "items": {
19883
+ "type": "object",
19884
+ "properties": {
19885
+ "expected": {
19886
+ "type": "string",
19887
+ "enum": ["match", "no_match"]
19888
+ },
19889
+ "label": {
19890
+ "type": ["string", "null"],
19891
+ "minLength": 1,
19892
+ "maxLength": 200,
19893
+ "pattern": "^[^\\u0000]*$",
19894
+ "description": "What the email stands for, such as a personal medical bill."
19895
+ },
19896
+ "email": {
19897
+ "type": "object",
19898
+ "properties": {
19899
+ "from": {
19900
+ "type": "object",
19901
+ "properties": {
19902
+ "address": {
19903
+ "type": "string",
19904
+ "maxLength": 254,
19905
+ "format": "email",
19906
+ "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,}$"
19907
+ },
19908
+ "name": {
19909
+ "type": "string",
19910
+ "maxLength": 200,
19911
+ "pattern": "^[^\\u0000]*$"
19912
+ }
19913
+ },
19914
+ "required": ["address"]
19915
+ },
19916
+ "to": {
19917
+ "minItems": 1,
19918
+ "maxItems": 10,
19919
+ "type": "array",
19920
+ "items": {
19921
+ "type": "string",
19922
+ "maxLength": 254,
19923
+ "format": "email",
19924
+ "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,}$"
19925
+ }
19926
+ },
19927
+ "cc": {
19928
+ "maxItems": 10,
19929
+ "type": "array",
19930
+ "items": {
19931
+ "type": "string",
19932
+ "maxLength": 254,
19933
+ "format": "email",
19934
+ "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,}$"
19935
+ }
19936
+ },
19937
+ "subject": {
19938
+ "type": "string",
19939
+ "maxLength": 998,
19940
+ "pattern": "^[^\\u0000]*$"
19941
+ },
19942
+ "text": {
19943
+ "type": "string",
19944
+ "minLength": 1,
19945
+ "maxLength": 4e3,
19946
+ "pattern": "^[^\\u0000]*$"
19947
+ },
19948
+ "attachment_types": {
19949
+ "maxItems": 10,
19950
+ "type": "array",
19951
+ "items": {
19952
+ "type": "string",
19953
+ "minLength": 1,
19954
+ "maxLength": 255,
19955
+ "pattern": "^[^\\u0000]*$"
19956
+ }
19957
+ },
19958
+ "automated": { "type": "boolean" },
19959
+ "from_mailing_list": { "type": "boolean" },
19960
+ "spam_score": {
19961
+ "type": "number",
19962
+ "minimum": -1e3,
19963
+ "maximum": 1e3
19964
+ }
19965
+ },
19966
+ "required": [
19967
+ "from",
19968
+ "to",
19969
+ "text"
19970
+ ],
19971
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
19972
+ },
19973
+ "id": {
19974
+ "type": "string",
19975
+ "format": "uuid"
19976
+ },
19977
+ "created_at": { "type": "string" }
19978
+ },
19979
+ "required": ["expected", "email"],
19980
+ "additionalProperties": false
19981
+ },
19982
+ "description": "Replaces its saved made-up emails (author only); null or [] removes them."
19983
+ },
19984
+ "add_expectation_samples": {
19985
+ "description": "Adds made-up emails to those saved (author only), up to 20 in all.",
19986
+ "minItems": 1,
19987
+ "maxItems": 20,
19988
+ "type": "array",
19989
+ "items": {
19990
+ "type": "object",
19991
+ "properties": {
19992
+ "expected": {
19993
+ "type": "string",
19994
+ "enum": ["match", "no_match"]
19995
+ },
19996
+ "label": {
19997
+ "type": ["string", "null"],
19998
+ "minLength": 1,
19999
+ "maxLength": 200,
20000
+ "pattern": "^[^\\u0000]*$",
20001
+ "description": "What the email stands for, such as a personal medical bill."
20002
+ },
20003
+ "email": {
20004
+ "type": "object",
20005
+ "properties": {
20006
+ "from": {
20007
+ "type": "object",
20008
+ "properties": {
20009
+ "address": {
20010
+ "type": "string",
20011
+ "maxLength": 254,
20012
+ "format": "email",
20013
+ "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,}$"
20014
+ },
20015
+ "name": {
20016
+ "type": "string",
20017
+ "maxLength": 200,
20018
+ "pattern": "^[^\\u0000]*$"
20019
+ }
20020
+ },
20021
+ "required": ["address"]
20022
+ },
20023
+ "to": {
20024
+ "minItems": 1,
20025
+ "maxItems": 10,
20026
+ "type": "array",
20027
+ "items": {
20028
+ "type": "string",
20029
+ "maxLength": 254,
20030
+ "format": "email",
20031
+ "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,}$"
20032
+ }
20033
+ },
20034
+ "cc": {
20035
+ "maxItems": 10,
20036
+ "type": "array",
20037
+ "items": {
20038
+ "type": "string",
20039
+ "maxLength": 254,
20040
+ "format": "email",
20041
+ "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,}$"
20042
+ }
20043
+ },
20044
+ "subject": {
20045
+ "type": "string",
20046
+ "maxLength": 998,
20047
+ "pattern": "^[^\\u0000]*$"
20048
+ },
20049
+ "text": {
20050
+ "type": "string",
20051
+ "minLength": 1,
20052
+ "maxLength": 4e3,
20053
+ "pattern": "^[^\\u0000]*$"
20054
+ },
20055
+ "attachment_types": {
20056
+ "maxItems": 10,
20057
+ "type": "array",
20058
+ "items": {
20059
+ "type": "string",
20060
+ "minLength": 1,
20061
+ "maxLength": 255,
20062
+ "pattern": "^[^\\u0000]*$"
20063
+ }
20064
+ },
20065
+ "automated": { "type": "boolean" },
20066
+ "from_mailing_list": { "type": "boolean" },
20067
+ "spam_score": {
20068
+ "type": "number",
20069
+ "minimum": -1e3,
20070
+ "maximum": 1e3
20071
+ }
20072
+ },
20073
+ "required": [
20074
+ "from",
20075
+ "to",
20076
+ "text"
20077
+ ],
20078
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
20079
+ },
20080
+ "id": {
20081
+ "type": "string",
20082
+ "format": "uuid"
20083
+ },
20084
+ "created_at": { "type": "string" }
20085
+ },
20086
+ "required": ["expected", "email"],
20087
+ "additionalProperties": false
20088
+ }
20089
+ },
20090
+ "if_revision": {
20091
+ "description": "The revision you read: if the rule was updated since, nothing is saved (409 email_rule_conflict, revision_moved).",
20092
+ "type": "integer",
20093
+ "minimum": 1,
20094
+ "maximum": 2147483647
20095
+ },
20096
+ "if_expectations_pass": {
20097
+ "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.",
20098
+ "type": "boolean"
20099
+ },
20100
+ "restore_version": {
20101
+ "description": "Save version n again as the next version (author only): its english, tree, explanation, assumptions, warnings and compiled_by. A version carries no scope: the rule keeps its own unless evaluation_scope is sent. 404 version_not_found; 400 already_current or version_not_renderable. Not with a version's own fields.",
20102
+ "type": "integer",
20103
+ "minimum": 1,
20104
+ "maximum": 2147483647
18605
20105
  }
18606
20106
  },
18607
20107
  "additionalProperties": false
@@ -18627,6 +20127,10 @@ const operationManifest = [
18627
20127
  "type": "string",
18628
20128
  "enum": ["grant", "do_not_share"]
18629
20129
  },
20130
+ "protection_note": {
20131
+ "description": "On a do_not_share rule only: it protects nothing yet (not active), or a classifier fact narrows it.",
20132
+ "type": "string"
20133
+ },
18630
20134
  "evaluation_scope": {
18631
20135
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
18632
20136
  "type": "string",
@@ -18657,10 +20161,37 @@ const operationManifest = [
18657
20161
  "type": ["integer", "null"],
18658
20162
  "exclusiveMinimum": 0
18659
20163
  },
20164
+ "revision": {
20165
+ "description": "Moves on by one with every update. Send it as if_revision.",
20166
+ "type": "integer",
20167
+ "exclusiveMinimum": 0
20168
+ },
18660
20169
  "author_user_id": {
18661
20170
  "type": "string",
18662
20171
  "format": "uuid"
18663
20172
  },
20173
+ "author": {
20174
+ "type": "object",
20175
+ "properties": {
20176
+ "user_id": {
20177
+ "type": "string",
20178
+ "format": "uuid"
20179
+ },
20180
+ "name": {
20181
+ "description": "Their name; null unless you can list members or this is you, and when they have none set.",
20182
+ "type": ["string", "null"]
20183
+ },
20184
+ "email": {
20185
+ "description": "Sign-in email; null unless you can list member emails or this is you.",
20186
+ "type": ["string", "null"]
20187
+ }
20188
+ },
20189
+ "required": [
20190
+ "user_id",
20191
+ "name",
20192
+ "email"
20193
+ ]
20194
+ },
18664
20195
  "is_author": {
18665
20196
  "type": "boolean",
18666
20197
  "description": "Whether you are its author, in your current membership. Only its author edits it; others who see it may only disable or delete it (while shared, only if they manage members)."
@@ -18673,91 +20204,94 @@ const operationManifest = [
18673
20204
  "type": "string",
18674
20205
  "format": "date-time"
18675
20206
  },
18676
- "current": { "anyOf": [{
18677
- "type": "object",
18678
- "properties": {
18679
- "version": {
18680
- "type": "integer",
18681
- "exclusiveMinimum": 0
18682
- },
18683
- "english": { "type": "string" },
18684
- "tree": {
18685
- "type": "object",
18686
- "properties": {
18687
- "version": {
18688
- "type": "number",
18689
- "const": 1
18690
- },
18691
- "vocabulary": {
18692
- "type": "string",
18693
- "description": "Vocabulary version, such as v1."
18694
- },
18695
- "when": {
18696
- "type": "object",
18697
- "propertyNames": { "type": "string" },
18698
- "additionalProperties": {},
18699
- "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
18700
- },
18701
- "questions": {
18702
- "type": "array",
18703
- "items": { "type": "string" },
18704
- "description": "Yes/no questions asked once the conditions match; all must be yes."
18705
- }
20207
+ "current": {
20208
+ "description": "Its current version: its text (english, tree, explanation, assumptions, warnings) and who compiled it. current_version is its number. GET /email-rules lists rules without it.",
20209
+ "anyOf": [{
20210
+ "type": "object",
20211
+ "properties": {
20212
+ "version": {
20213
+ "type": "integer",
20214
+ "exclusiveMinimum": 0
18706
20215
  },
18707
- "required": [
18708
- "version",
18709
- "vocabulary",
18710
- "when",
18711
- "questions"
18712
- ]
18713
- },
18714
- "explanation": { "type": "string" },
18715
- "assumptions": {
18716
- "type": "array",
18717
- "items": { "type": "string" }
18718
- },
18719
- "warnings": {
18720
- "type": "array",
18721
- "items": { "type": "string" }
18722
- },
18723
- "compiled_by": {
18724
- "type": "string",
18725
- "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
18726
- },
18727
- "vocabulary": { "type": "string" },
18728
- "created_at": {
18729
- "type": "string",
18730
- "format": "date-time"
18731
- },
18732
- "sql": {
18733
- "description": "Present only when include_sql was true.",
18734
- "type": "object",
18735
- "properties": {
18736
- "expression": {
18737
- "type": "string",
18738
- "description": "A parameterized boolean expression over one row per email."
20216
+ "english": { "type": "string" },
20217
+ "tree": {
20218
+ "type": "object",
20219
+ "properties": {
20220
+ "version": {
20221
+ "type": "number",
20222
+ "const": 1
20223
+ },
20224
+ "vocabulary": {
20225
+ "type": "string",
20226
+ "description": "Vocabulary version, such as v1."
20227
+ },
20228
+ "when": {
20229
+ "type": "object",
20230
+ "propertyNames": { "type": "string" },
20231
+ "additionalProperties": {},
20232
+ "description": "A condition {column, op, value?, min_confidence?} (see GET /email-rules/vocabulary) or {all: [...]}, {any: [...]}, {not: ...}."
20233
+ },
20234
+ "questions": {
20235
+ "type": "array",
20236
+ "items": { "type": "string" },
20237
+ "description": "Yes/no questions asked once the conditions match; all must be yes."
20238
+ }
18739
20239
  },
18740
- "params": {
18741
- "type": "array",
18742
- "items": {},
18743
- "description": "The values bound to $1, $2 and so on."
18744
- }
20240
+ "required": [
20241
+ "version",
20242
+ "vocabulary",
20243
+ "when",
20244
+ "questions"
20245
+ ]
18745
20246
  },
18746
- "required": ["expression", "params"]
18747
- }
18748
- },
18749
- "required": [
18750
- "version",
18751
- "english",
18752
- "tree",
18753
- "explanation",
18754
- "assumptions",
18755
- "warnings",
18756
- "compiled_by",
18757
- "vocabulary",
18758
- "created_at"
18759
- ]
18760
- }, { "type": "null" }] },
20247
+ "explanation": { "type": "string" },
20248
+ "assumptions": {
20249
+ "type": "array",
20250
+ "items": { "type": "string" }
20251
+ },
20252
+ "warnings": {
20253
+ "type": "array",
20254
+ "items": { "type": "string" }
20255
+ },
20256
+ "compiled_by": {
20257
+ "type": "string",
20258
+ "description": "Who produced the tree, as stated when saved (not verified), such as rule-compiler-v1 or member."
20259
+ },
20260
+ "vocabulary": { "type": "string" },
20261
+ "created_at": {
20262
+ "type": "string",
20263
+ "format": "date-time"
20264
+ },
20265
+ "sql": {
20266
+ "description": "Present only when include_sql was true.",
20267
+ "type": "object",
20268
+ "properties": {
20269
+ "expression": {
20270
+ "type": "string",
20271
+ "description": "A parameterized boolean expression over one row per email."
20272
+ },
20273
+ "params": {
20274
+ "type": "array",
20275
+ "items": {},
20276
+ "description": "The values bound to $1, $2 and so on."
20277
+ }
20278
+ },
20279
+ "required": ["expression", "params"]
20280
+ }
20281
+ },
20282
+ "required": [
20283
+ "version",
20284
+ "english",
20285
+ "tree",
20286
+ "explanation",
20287
+ "assumptions",
20288
+ "warnings",
20289
+ "compiled_by",
20290
+ "vocabulary",
20291
+ "created_at"
20292
+ ]
20293
+ }, { "type": "null" }]
20294
+ },
18761
20295
  "scope_warnings": {
18762
20296
  "description": "Names in scope_selection that match nothing this organization receives mail at (a typo, or an inbox not yet in use). Present only when there are some.",
18763
20297
  "type": "array",
@@ -18788,8 +20322,114 @@ const operationManifest = [
18788
20322
  ]
18789
20323
  }
18790
20324
  },
20325
+ "expectation_samples": {
20326
+ "description": "Its saved made-up emails, when this write set or added some.",
20327
+ "type": "array",
20328
+ "items": {
20329
+ "type": "object",
20330
+ "properties": {
20331
+ "id": {
20332
+ "type": "string",
20333
+ "format": "uuid"
20334
+ },
20335
+ "expected": {
20336
+ "type": "string",
20337
+ "enum": ["match", "no_match"]
20338
+ },
20339
+ "label": { "type": ["string", "null"] },
20340
+ "email": {
20341
+ "type": "object",
20342
+ "properties": {
20343
+ "from": {
20344
+ "type": "object",
20345
+ "properties": {
20346
+ "address": {
20347
+ "type": "string",
20348
+ "maxLength": 254,
20349
+ "format": "email",
20350
+ "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,}$"
20351
+ },
20352
+ "name": {
20353
+ "type": "string",
20354
+ "maxLength": 200,
20355
+ "pattern": "^[^\\u0000]*$"
20356
+ }
20357
+ },
20358
+ "required": ["address"]
20359
+ },
20360
+ "to": {
20361
+ "minItems": 1,
20362
+ "maxItems": 10,
20363
+ "type": "array",
20364
+ "items": {
20365
+ "type": "string",
20366
+ "maxLength": 254,
20367
+ "format": "email",
20368
+ "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,}$"
20369
+ }
20370
+ },
20371
+ "cc": {
20372
+ "maxItems": 10,
20373
+ "type": "array",
20374
+ "items": {
20375
+ "type": "string",
20376
+ "maxLength": 254,
20377
+ "format": "email",
20378
+ "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,}$"
20379
+ }
20380
+ },
20381
+ "subject": {
20382
+ "type": "string",
20383
+ "maxLength": 998,
20384
+ "pattern": "^[^\\u0000]*$"
20385
+ },
20386
+ "text": {
20387
+ "type": "string",
20388
+ "minLength": 1,
20389
+ "maxLength": 4e3,
20390
+ "pattern": "^[^\\u0000]*$"
20391
+ },
20392
+ "attachment_types": {
20393
+ "maxItems": 10,
20394
+ "type": "array",
20395
+ "items": {
20396
+ "type": "string",
20397
+ "minLength": 1,
20398
+ "maxLength": 255,
20399
+ "pattern": "^[^\\u0000]*$"
20400
+ }
20401
+ },
20402
+ "automated": { "type": "boolean" },
20403
+ "from_mailing_list": { "type": "boolean" },
20404
+ "spam_score": {
20405
+ "type": "number",
20406
+ "minimum": -1e3,
20407
+ "maximum": 1e3
20408
+ }
20409
+ },
20410
+ "required": [
20411
+ "from",
20412
+ "to",
20413
+ "text"
20414
+ ],
20415
+ "description": "A made-up email, as POST /email-rules/test-sample takes it."
20416
+ },
20417
+ "created_at": {
20418
+ "type": "string",
20419
+ "format": "date-time"
20420
+ }
20421
+ },
20422
+ "required": [
20423
+ "id",
20424
+ "expected",
20425
+ "label",
20426
+ "email",
20427
+ "created_at"
20428
+ ]
20429
+ }
20430
+ },
18791
20431
  "expectations_check": {
18792
- "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.",
20432
+ "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.",
18793
20433
  "type": "object",
18794
20434
  "properties": {
18795
20435
  "version": {
@@ -18801,7 +20441,7 @@ const operationManifest = [
18801
20441
  "type": ["boolean", "null"]
18802
20442
  },
18803
20443
  "decided": {
18804
- "description": "Whether every expectation was judged (undecided is empty). Absent from a server that predates it.",
20444
+ "description": "Whether every expectation was judged (undecided and undecided_samples are empty). Absent from a server that predates it.",
18805
20445
  "type": "boolean"
18806
20446
  },
18807
20447
  "failed": {
@@ -18817,6 +20457,26 @@ const operationManifest = [
18817
20457
  "type": "string",
18818
20458
  "format": "uuid"
18819
20459
  }
20460
+ },
20461
+ "failed_samples": {
20462
+ "description": "Made-up emails that failed, by index in the set judged (saved oldest first, then added; or as sent).",
20463
+ "type": "array",
20464
+ "items": {
20465
+ "type": "integer",
20466
+ "minimum": 0
20467
+ }
20468
+ },
20469
+ "undecided_samples": {
20470
+ "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.",
20471
+ "type": "array",
20472
+ "items": {
20473
+ "type": "integer",
20474
+ "minimum": 0
20475
+ }
20476
+ },
20477
+ "note": {
20478
+ "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.",
20479
+ "type": "string"
18820
20480
  }
18821
20481
  },
18822
20482
  "required": [
@@ -18825,6 +20485,41 @@ const operationManifest = [
18825
20485
  "failed",
18826
20486
  "undecided"
18827
20487
  ]
20488
+ },
20489
+ "restored_from": {
20490
+ "description": "When this write saved an earlier version again (restore_version): which, and that the scope is not part of a version.",
20491
+ "type": "object",
20492
+ "properties": {
20493
+ "version": {
20494
+ "type": "integer",
20495
+ "exclusiveMinimum": 0
20496
+ },
20497
+ "note": { "type": "string" }
20498
+ },
20499
+ "required": ["version", "note"]
20500
+ },
20501
+ "affects": {
20502
+ "description": "After an update that saved a version: API keys listing the rule, the webhooks they own, and shares this write ended. The rule keeps its state. Absent if not read.",
20503
+ "type": "object",
20504
+ "properties": {
20505
+ "keys": {
20506
+ "type": "integer",
20507
+ "minimum": 0
20508
+ },
20509
+ "endpoints": {
20510
+ "type": "integer",
20511
+ "minimum": 0
20512
+ },
20513
+ "shares_ended": {
20514
+ "type": "integer",
20515
+ "minimum": 0
20516
+ }
20517
+ },
20518
+ "required": [
20519
+ "keys",
20520
+ "endpoints",
20521
+ "shares_ended"
20522
+ ]
18828
20523
  }
18829
20524
  },
18830
20525
  "required": [
@@ -19018,6 +20713,14 @@ const operationManifest = [
19018
20713
  "decline_reason": {
19019
20714
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
19020
20715
  "type": ["string", "null"]
20716
+ },
20717
+ "edited_on_accept": {
20718
+ "description": "Once accepted: whether the rule was changed first, as on the share. Null until then, or when unreadable.",
20719
+ "type": ["boolean", "null"]
20720
+ },
20721
+ "notice": {
20722
+ "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.",
20723
+ "type": ["string", "null"]
19021
20724
  }
19022
20725
  },
19023
20726
  "required": [
@@ -35034,7 +36737,7 @@ const operationManifest = [
35034
36737
  "binaryResponse": false,
35035
36738
  "bodyRequired": true,
35036
36739
  "command": "decide-suspension",
35037
- "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.",
36740
+ "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.",
35038
36741
  "hasJsonBody": true,
35039
36742
  "method": "POST",
35040
36743
  "operationId": "decideSuspension",
@@ -46372,11 +48075,11 @@ const operationManifest = [
46372
48075
  "description": "Whether this is the latest attempt of its delivery: the one recorded last, where a record of no attempt (a rule skip or a held send, attempt_count 0) is latest only when no attempt was made. A retry is recorded as a new attempt with its own id and attempt_count (not always higher than the one before), so an earlier attempt never changes: follow the latest one, or read attempts on any of them."
46373
48076
  },
46374
48077
  "retry_scheduled": {
46375
- "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. Null on every attempt of a delivery retried or settled on a schedule of its own, which this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, a rule-granted one, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will.",
48078
+ "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. A rule-granted delivery is retried on a schedule of its own, to the same endpoint, deciding it afresh: on its latest attempt true while another attempt is to come (the delivery's next_attempt_at says when); null while an attempt is under way (read it again shortly), while it waits on an owner who cannot receive mail or on a held endpoint (checked again hourly, and sent once that clears), when it is past its 7-day limit, or when that could not be read just now; and false otherwise, including once the endpoint is off or already has the email. Null on every attempt of a delivery retried or settled on a schedule of its own that this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will.",
46376
48079
  "type": ["boolean", "null"]
46377
48080
  },
46378
48081
  "next_attempt_at": {
46379
- "description": "When the next automatic retry of this delivery is due. Not reported: always null. Whether one is coming is retry_scheduled.",
48082
+ "description": "When the next automatic attempt of a rule-granted delivery is due: set on its latest attempt while retry_scheduled is true. The attempt runs at the first retry pass at or after this time (one runs every few minutes), so a time already past means it is due now. Null otherwise, and on every delivery that is not rule-granted, whose retries are passes for the email with no time reported (whether one is coming is retry_scheduled).",
46380
48083
  "type": ["string", "null"]
46381
48084
  },
46382
48085
  "attempts": {
@@ -46406,7 +48109,7 @@ const operationManifest = [
46406
48109
  "type": ["string", "null"]
46407
48110
  },
46408
48111
  "retry_scheduled": {
46409
- "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. Null on every attempt of a delivery retried or settled on a schedule of its own, which this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, a rule-granted one, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will.",
48112
+ "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. A rule-granted delivery is retried on a schedule of its own, to the same endpoint, deciding it afresh: on its latest attempt true while another attempt is to come (the delivery's next_attempt_at says when); null while an attempt is under way (read it again shortly), while it waits on an owner who cannot receive mail or on a held endpoint (checked again hourly, and sent once that clears), when it is past its 7-day limit, or when that could not be read just now; and false otherwise, including once the endpoint is off or already has the email. Null on every attempt of a delivery retried or settled on a schedule of its own that this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will.",
46410
48113
  "type": ["boolean", "null"]
46411
48114
  }
46412
48115
  },
@@ -46620,12 +48323,12 @@ const operationManifest = [
46620
48323
  },
46621
48324
  "retry_scheduled": {
46622
48325
  "type": ["boolean", "null"],
46623
- "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. Null on every attempt of a delivery retried or settled on a schedule of its own, which this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, a rule-granted one, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will."
48326
+ "description": "Whether an automatic retry will try this delivery again. A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh (its rules, its owner's access, whether it is still on), so true promises a pass, not a delivery to this endpoint. A rule-granted delivery is retried on a schedule of its own, to the same endpoint, deciding it afresh: on its latest attempt true while another attempt is to come (the delivery's next_attempt_at says when); null while an attempt is under way (read it again shortly), while it waits on an owner who cannot receive mail or on a held endpoint (checked again hourly, and sent once that clears), when it is past its 7-day limit, or when that could not be read just now; and false otherwise, including once the endpoint is off or already has the email. Null on every attempt of a delivery retried or settled on a schedule of its own that this does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, and a sent email's or other event's delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery: null while a delivery pass for the email is running (whether another follows is decided when it ends), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. False on every other attempt, including every attempt a later one followed: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will."
46624
48327
  },
46625
48328
  "next_attempt_at": {
46626
48329
  "type": ["string", "null"],
46627
48330
  "format": "date-time",
46628
- "description": "When the next automatic retry of this delivery is due. Not reported: always null. Whether one is coming is retry_scheduled."
48331
+ "description": "When the next automatic attempt of a rule-granted delivery is due: set on its latest attempt while retry_scheduled is true. The attempt runs at the first retry pass at or after this time (one runs every few minutes), so a time already past means it is due now. Null otherwise, and on every delivery that is not rule-granted, whose retries are passes for the email with no time reported (whether one is coming is retry_scheduled)."
46629
48332
  }
46630
48333
  },
46631
48334
  "required": [
@@ -46834,7 +48537,7 @@ function sanitizeForTerminal(value) {
46834
48537
  return out;
46835
48538
  }
46836
48539
  // src/oclif/answer-reports.ts
46837
- function isRecord$2(value) {
48540
+ function isRecord$3(value) {
46838
48541
  return value !== null && typeof value === "object" && !Array.isArray(value);
46839
48542
  }
46840
48543
  /** Text from an answer, made safe and one line for a report: no terminal controls. */
@@ -46843,10 +48546,10 @@ function oneLine(text) {
46843
48546
  }
46844
48547
  /** One line per warning a route write answers with (createRoute, updateRoute). */
46845
48548
  function routeWarningLines(data) {
46846
- const warnings = isRecord$2(data) ? data.warnings : void 0;
48549
+ const warnings = isRecord$3(data) ? data.warnings : void 0;
46847
48550
  if (!Array.isArray(warnings)) return [];
46848
48551
  return warnings.flatMap((warning) => {
46849
- if (!isRecord$2(warning)) return [];
48552
+ if (!isRecord$3(warning)) return [];
46850
48553
  const message = typeof warning.message === "string" ? oneLine(warning.message) : "";
46851
48554
  if (message) return [`Route warning: ${withCliCommands(message)}`];
46852
48555
  const code = typeof warning.code === "string" ? oneLine(warning.code) : "";
@@ -46874,11 +48577,11 @@ const DELIVERED_STATUSES = /* @__PURE__ */ new Set(["delivered", "header_confirm
46874
48577
  * and has no recorded answer says so.
46875
48578
  */
46876
48579
  function deliveryAttemptLines(data) {
46877
- const attempts = isRecord$2(data) ? data.attempts : void 0;
48580
+ const attempts = isRecord$3(data) ? data.attempts : void 0;
46878
48581
  if (!Array.isArray(attempts)) return [];
46879
48582
  const lines = [];
46880
48583
  for (const attempt of attempts) {
46881
- if (!isRecord$2(attempt)) continue;
48584
+ if (!isRecord$3(attempt)) continue;
46882
48585
  const text = (value) => typeof value === "string" && value ? oneLine(value) : "";
46883
48586
  const status = text(attempt.status);
46884
48587
  const parts = [
@@ -46972,7 +48675,7 @@ var CliOrgRefusedError = class extends Errors.CLIError {
46972
48675
  this.orgId = params.orgId;
46973
48676
  }
46974
48677
  };
46975
- function isRecord$1(value) {
48678
+ function isRecord$2(value) {
46976
48679
  return value !== null && typeof value === "object" && !Array.isArray(value);
46977
48680
  }
46978
48681
  function apiOrigin(apiBaseUrl) {
@@ -46999,7 +48702,7 @@ function isUuid(value) {
46999
48702
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
47000
48703
  }
47001
48704
  function isOrgAccessToken(value) {
47002
- 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";
48705
+ 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";
47003
48706
  }
47004
48707
  function orgTokenFromResponse(token, now = Date.now) {
47005
48708
  return {
@@ -47046,7 +48749,7 @@ async function postTokenEndpoint(params) {
47046
48749
  throw new Errors.CLIError(`Could not reach the Primitive OAuth token endpoint: ${detail}`, { exit: 1 });
47047
48750
  }
47048
48751
  const payload = await response.json().catch(() => null);
47049
- if (response.ok && isRecord$1(payload)) return {
48752
+ if (response.ok && isRecord$2(payload)) return {
47050
48753
  ok: true,
47051
48754
  payload
47052
48755
  };
@@ -47054,7 +48757,7 @@ async function postTokenEndpoint(params) {
47054
48757
  ok: true,
47055
48758
  payload: {}
47056
48759
  };
47057
- const record = isRecord$1(payload) ? payload : {};
48760
+ const record = isRecord$2(payload) ? payload : {};
47058
48761
  return {
47059
48762
  ok: false,
47060
48763
  status: response.status,
@@ -47257,12 +48960,12 @@ async function fetchCliOrgs(params) {
47257
48960
  }
47258
48961
  const payload = await response.json().catch(() => null);
47259
48962
  if (!response.ok) {
47260
- const error = isRecord$1(payload) && isRecord$1(payload.error) ? payload.error : {};
48963
+ const error = isRecord$2(payload) && isRecord$2(payload.error) ? payload.error : {};
47261
48964
  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 });
47262
48965
  throw new Errors.CLIError(`Could not list organizations${typeof error.message === "string" ? `: ${error.message}` : ` (HTTP ${response.status}).`}`, { exit: 1 });
47263
48966
  }
47264
- const data = isRecord$1(payload) ? payload.data : null;
47265
- if (!isRecord$1(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
48967
+ const data = isRecord$2(payload) ? payload.data : null;
48968
+ if (!isRecord$2(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
47266
48969
  return data;
47267
48970
  }
47268
48971
  /** How long a command waits for another CLI process to finish with the saved login. */
@@ -47835,6 +49538,358 @@ async function withAttachmentWait(call, options) {
47835
49538
  }
47836
49539
  return result;
47837
49540
  }
49541
+ // src/oclif/email-rule-samples.ts
49542
+ function errorDetail(error) {
49543
+ return error instanceof Error ? error.message : String(error);
49544
+ }
49545
+ function usageError$1(message) {
49546
+ return new Errors.CLIError(message, { exit: 2 });
49547
+ }
49548
+ function isRecord$1(value) {
49549
+ return value !== null && typeof value === "object" && !Array.isArray(value);
49550
+ }
49551
+ /** A path as a shell word: quoted when it holds anything but plain characters. */
49552
+ function shellWord(value) {
49553
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
49554
+ }
49555
+ /** A JSON file a flag names, parsed, or an exit-1 error saying why not. */
49556
+ function readJsonFileFlag(path, flag) {
49557
+ let text;
49558
+ try {
49559
+ text = readFileSync(path, "utf8");
49560
+ } catch (error) {
49561
+ throw new Errors.CLIError(`Could not read ${flag} ${path}: ${errorDetail(error)}`, { exit: 1 });
49562
+ }
49563
+ try {
49564
+ return JSON.parse(text);
49565
+ } catch (error) {
49566
+ throw new Errors.CLIError(`${flag} ${path} is not valid JSON: ${errorDetail(error)}`, { exit: 1 });
49567
+ }
49568
+ }
49569
+ /** The longest label the API keeps on a sample. */
49570
+ const SAMPLE_LABEL_MAX = 200;
49571
+ /**
49572
+ * `text` cut to `max` UTF-16 units, as the API counts a label's length,
49573
+ * never inside a character.
49574
+ */
49575
+ function cutLabel(text, max) {
49576
+ const cut = text.slice(0, max);
49577
+ return (/[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut).trimEnd();
49578
+ }
49579
+ /**
49580
+ * A look-alike as a must-not-match sample: the email as written, labelled
49581
+ * with its kind and why the rule should not match it ("medical: ..."), or
49582
+ * the why alone when both do not fit a label.
49583
+ */
49584
+ function lookAlikeSample(lookAlike) {
49585
+ if (!isRecord$1(lookAlike) || !isRecord$1(lookAlike.email)) return null;
49586
+ const kind = typeof lookAlike.kind === "string" ? lookAlike.kind : "";
49587
+ const why = typeof lookAlike.why === "string" ? lookAlike.why.trim() : "";
49588
+ const both = kind && why ? `${kind}: ${why}` : kind || why;
49589
+ const label = both.length <= SAMPLE_LABEL_MAX ? both : why.length <= SAMPLE_LABEL_MAX ? why : `${cutLabel(why, 197)}...`;
49590
+ return {
49591
+ expected: "no_match",
49592
+ ...label ? { label } : {},
49593
+ email: lookAlike.email
49594
+ };
49595
+ }
49596
+ /**
49597
+ * The look-alikes of a generate-look-alikes answer that can be saved (each
49598
+ * with an email), in the answer's order. The report numbers these and the
49599
+ * samples file holds these, so a number means the same email in both.
49600
+ */
49601
+ function lookAlikeEntries(answer) {
49602
+ const list = isRecord$1(answer) && Array.isArray(answer.look_alikes) ? answer.look_alikes : [];
49603
+ const entries = [];
49604
+ for (const item of list) {
49605
+ const sample = lookAlikeSample(item);
49606
+ if (sample && isRecord$1(item)) entries.push({
49607
+ lookAlike: item,
49608
+ sample
49609
+ });
49610
+ }
49611
+ return entries;
49612
+ }
49613
+ /** The look-alikes of a generate-look-alikes answer, as samples, in order. */
49614
+ function lookAlikeSamples(answer) {
49615
+ return lookAlikeEntries(answer).map((entry) => entry.sample);
49616
+ }
49617
+ /** The fields a saved made-up email takes, and the API's bounds on them. */
49618
+ const SAMPLE_EMAIL_KEYS = /* @__PURE__ */ new Set([
49619
+ "from",
49620
+ "to",
49621
+ "cc",
49622
+ "subject",
49623
+ "text",
49624
+ "attachment_types",
49625
+ "automated",
49626
+ "from_mailing_list",
49627
+ "spam_score"
49628
+ ]);
49629
+ const SAMPLE_TEXT_MAX = 4e3;
49630
+ const SAMPLE_SUBJECT_MAX = 998;
49631
+ const SAMPLE_LIST_MAX = 10;
49632
+ /**
49633
+ * Refuse, naming the sample and the field, a made-up email the API would
49634
+ * refuse to keep: the fields it takes and their bounds. The API checks the
49635
+ * rest (address forms, a NUL).
49636
+ */
49637
+ function checkSampleEmail(email, at) {
49638
+ const fail = (what) => {
49639
+ throw usageError$1(`${at}: email ${what}.`);
49640
+ };
49641
+ const unknown = Object.keys(email).find((key) => !SAMPLE_EMAIL_KEYS.has(key));
49642
+ if (unknown !== void 0) fail(`takes no "${unknown}" (it takes ${[...SAMPLE_EMAIL_KEYS].join(", ")})`);
49643
+ if (!isRecord$1(email.from) || typeof email.from.address !== "string") fail("needs from.address");
49644
+ const list = (key, min) => {
49645
+ const value = email[key];
49646
+ if (value === void 0 && min === 0) return;
49647
+ 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`);
49648
+ };
49649
+ list("to", 1);
49650
+ list("cc", 0);
49651
+ list("attachment_types", 0);
49652
+ 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`);
49653
+ 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`);
49654
+ for (const key of ["automated", "from_mailing_list"]) if (email[key] !== void 0 && typeof email[key] !== "boolean") fail(`${key} must be true or false`);
49655
+ if (email.spam_score !== void 0 && (typeof email.spam_score !== "number" || !Number.isFinite(email.spam_score))) fail("spam_score must be a number");
49656
+ }
49657
+ /** The keys a sample may carry: a saved one's id and time are taken and ignored. */
49658
+ const SAMPLE_KEYS = /* @__PURE__ */ new Set([
49659
+ "expected",
49660
+ "label",
49661
+ "email",
49662
+ "id",
49663
+ "created_at"
49664
+ ]);
49665
+ /**
49666
+ * The samples a file holds: a JSON array of samples, as --samples-file
49667
+ * writes them, or a generate-look-alikes answer saved as the command printed
49668
+ * it (with or without --json), whose look-alikes become must-not-match
49669
+ * samples numbered as its report numbered them. Only a literal [] holds no
49670
+ * samples; an answer with none is refused, so it never empties a rule's.
49671
+ */
49672
+ function samplesFromJson(value, source) {
49673
+ const data = isRecord$1(value) && isRecord$1(value.data) ? value.data : value;
49674
+ if (isRecord$1(data) && Array.isArray(data.look_alikes)) {
49675
+ const samples = lookAlikeSamples(data);
49676
+ if (samples.length === 0) throw usageError$1(`${source} is a generate-look-alikes answer with no look-alikes in it.`);
49677
+ samples.forEach((sample, index) => {
49678
+ checkSampleEmail(sample.email, `${source}, look-alike ${index + 1}`);
49679
+ });
49680
+ return samples;
49681
+ }
49682
+ 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.`);
49683
+ return data.map((item, index) => {
49684
+ const at = `${source}, sample ${index + 1}`;
49685
+ if (!isRecord$1(item)) throw usageError$1(`${at} is not an object.`);
49686
+ const unknown = Object.keys(item).find((key) => !SAMPLE_KEYS.has(key));
49687
+ if (unknown !== void 0) throw usageError$1(`${at}: "${unknown}" is not a sample field; a sample takes expected, label and email.`);
49688
+ if (item.expected !== "match" && item.expected !== "no_match") throw usageError$1(`${at}: expected must be "match" or "no_match".`);
49689
+ if (!isRecord$1(item.email)) throw usageError$1(`${at}: email must be an object, as test-email-rule-on-sample takes it.`);
49690
+ checkSampleEmail(item.email, at);
49691
+ const label = item.label;
49692
+ if (label !== void 0 && label !== null) {
49693
+ if (typeof label !== "string") throw usageError$1(`${at}: label must be a string.`);
49694
+ if (label.trim() === "") throw usageError$1(`${at}: label is blank; leave it out or say what the email stands for.`);
49695
+ if (label.trim().length > SAMPLE_LABEL_MAX) throw usageError$1(`${at}: label is longer than ${SAMPLE_LABEL_MAX} characters.`);
49696
+ }
49697
+ return {
49698
+ expected: item.expected,
49699
+ ...label !== void 0 ? { label } : {},
49700
+ email: item.email
49701
+ };
49702
+ });
49703
+ }
49704
+ /**
49705
+ * The numbers --pick names, from 1, in the order given and without repeats,
49706
+ * or a usage error: "2,4" or "2 4". `count` is how many there are to pick
49707
+ * from; a number past it is refused, rather than skipped.
49708
+ */
49709
+ function pickNumbers(pick, count, source) {
49710
+ const parts = pick.split(/[\s,]+/).map((part) => part.trim()).filter((part) => part !== "");
49711
+ if (parts.length === 0) throw usageError$1("--pick needs the numbers of the samples to take, such as --pick 2,4.");
49712
+ const chosen = [];
49713
+ for (const part of parts) {
49714
+ if (!/^\d+$/.test(part)) throw usageError$1(`--pick ${pick}: "${part}" is not a sample number.`);
49715
+ const n = Number(part);
49716
+ 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}`}.`);
49717
+ if (!chosen.includes(n)) chosen.push(n);
49718
+ }
49719
+ return chosen;
49720
+ }
49721
+ /** The samples --pick chooses, by their numbers from 1. */
49722
+ function pickSamples(samples, pick, source) {
49723
+ return pickNumbers(pick, samples.length, source).map((n) => samples[n - 1]);
49724
+ }
49725
+ /**
49726
+ * The number a person knows a sample a command sent by: its number in the
49727
+ * samples file when --pick took it, else its place from 1 in what was sent.
49728
+ * `index` is its place in what was sent, from 0, as the API gives it.
49729
+ */
49730
+ function sentSampleNumber(index, flags) {
49731
+ const pick = flags?.[PICK_FLAG];
49732
+ if (typeof pick !== "string") return index + 1;
49733
+ try {
49734
+ return pickNumbers(pick, Number.MAX_SAFE_INTEGER, "")[index] ?? index + 1;
49735
+ } catch {
49736
+ return index + 1;
49737
+ }
49738
+ }
49739
+ /** The samples file flags: replace (create and update), add (update), and test. */
49740
+ const SAMPLES_FILE_FLAG = "expectation-samples-file";
49741
+ const ADD_SAMPLES_FILE_FLAG = "add-expectation-samples-file";
49742
+ const EXPECT_SAMPLES_FILE_FLAG = "expect-samples-file";
49743
+ const PICK_FLAG = "pick";
49744
+ /** generate-look-alikes' flag for the file it writes them to. */
49745
+ const LOOK_ALIKES_SAMPLES_FILE_FLAG = "samples-file";
49746
+ /** The samples a samples file flag names, only those --pick chooses if given. */
49747
+ function readSamplesFile(path, flag, pick) {
49748
+ const source = `--${flag} ${path}`;
49749
+ const all = samplesFromJson(readJsonFileFlag(path, `--${flag}`), source);
49750
+ const samples = typeof pick === "string" ? pickSamples(all, pick, source) : all;
49751
+ 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.`);
49752
+ return samples;
49753
+ }
49754
+ /** The one of `names` given as a file path, or null; --pick without one is refused. */
49755
+ function givenSamplesFlag(flags, names) {
49756
+ const given = names.find((name) => typeof flags[name] === "string") ?? null;
49757
+ 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 ")}.`);
49758
+ return given;
49759
+ }
49760
+ /**
49761
+ * The body fields a samples file sets on create-email-rule and
49762
+ * update-email-rule: expectation_samples (on update it replaces them) or, on
49763
+ * update, add_expectation_samples. With --pick, only the samples it names.
49764
+ */
49765
+ function samplesBodyFields(canAdd) {
49766
+ const names = canAdd ? [SAMPLES_FILE_FLAG, ADD_SAMPLES_FILE_FLAG] : [SAMPLES_FILE_FLAG];
49767
+ return (flags) => {
49768
+ const flag = givenSamplesFlag(flags, names);
49769
+ if (flag === null) return [];
49770
+ const path = flags[flag];
49771
+ const samples = readSamplesFile(path, flag, flags[PICK_FLAG]);
49772
+ if (samples.length === 0 && !(canAdd && flag === "expectation-samples-file")) throw usageError$1(`--${flag} ${path} holds no samples.`);
49773
+ return [{
49774
+ flag: `--${flag}`,
49775
+ property: flag === "add-expectation-samples-file" ? "add_expectation_samples" : "expectation_samples",
49776
+ value: samples
49777
+ }];
49778
+ };
49779
+ }
49780
+ /**
49781
+ * The samples test-email-rule expects, from --expect-samples-file (only
49782
+ * those --pick chooses if given), or null without the flag. They go in
49783
+ * expect.samples.
49784
+ */
49785
+ function expectSamples(flags) {
49786
+ const flag = givenSamplesFlag(flags, [EXPECT_SAMPLES_FILE_FLAG]);
49787
+ if (flag === null) return null;
49788
+ const path = flags[flag];
49789
+ const samples = readSamplesFile(path, flag, flags[PICK_FLAG]);
49790
+ if (samples.length === 0) throw usageError$1(`--${flag} ${path} holds no samples.`);
49791
+ return samples;
49792
+ }
49793
+ /** --pick's help, naming the flags it chooses from. */
49794
+ function pickFlagHelp(flags) {
49795
+ 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.`;
49796
+ }
49797
+ /** What the rule decided for a look-alike, and what its status makes of it. */
49798
+ function decidedLine(outcome, status) {
49799
+ if (outcome === "not_decided") return "The rule did not decide it: it was not reached in time, or deciding it failed.";
49800
+ 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." : "The rule matched it, so mail like it would be shared.";
49801
+ const decided = outcome === "near_miss" ? "The rule nearly matched it, but did not" : "The rule did not match it";
49802
+ return status === "undecided" ? `${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.` : `${decided}.`;
49803
+ }
49804
+ /**
49805
+ * The stderr report of generate-look-alikes: each look-alike numbered as the
49806
+ * samples file numbers it, with its status, kind, subject, sender, why the
49807
+ * rule should not match it and what the rule decided; the counts; the
49808
+ * answer's note; and how to save the ones you choose. With --samples-file it
49809
+ * writes every look-alike there as a must-not-match sample, in that order.
49810
+ */
49811
+ function lookAlikesReport(data, request) {
49812
+ const file = request.flags?.[LOOK_ALIKES_SAMPLES_FILE_FLAG];
49813
+ if (!isRecord$1(data)) {
49814
+ if (typeof file !== "string") return { lines: [] };
49815
+ const message = `Did not write --${LOOK_ALIKES_SAMPLES_FILE_FLAG} ${file}: the answer held no look-alikes to write.`;
49816
+ return {
49817
+ lines: [message],
49818
+ exitCode: 1,
49819
+ error: {
49820
+ code: "samples_file_not_written",
49821
+ message
49822
+ }
49823
+ };
49824
+ }
49825
+ const entries = lookAlikeEntries(data);
49826
+ const lines = [];
49827
+ entries.forEach(({ lookAlike }, index) => {
49828
+ const email = lookAlike.email;
49829
+ const from = isRecord$1(email.from) && typeof email.from.address === "string" ? email.from.address : "an unknown sender";
49830
+ const subject = typeof email.subject === "string" && email.subject ? `"${email.subject}"` : "(no subject)";
49831
+ const status = typeof lookAlike.status === "string" ? lookAlike.status : "undecided";
49832
+ const kind = typeof lookAlike.kind === "string" ? lookAlike.kind : "other";
49833
+ const outcome = typeof lookAlike.outcome === "string" ? lookAlike.outcome : "not_decided";
49834
+ lines.push(`${index + 1}. ${status} ${kind} ${subject} from ${from}`);
49835
+ if (typeof lookAlike.why === "string" && lookAlike.why.trim()) lines.push(` Why it should not match: ${lookAlike.why.trim()}`);
49836
+ lines.push(` ${decidedLine(outcome, status)}`);
49837
+ });
49838
+ const counts = isRecord$1(data.counts) ? data.counts : {};
49839
+ const count = (key) => typeof counts[key] === "number" ? counts[key] : 0;
49840
+ const dropped = count("dropped");
49841
+ lines.push(`Counts: ${count("passed")} passed, ${count("failed")} failed, ${count("undecided")} undecided${dropped > 0 ? `; ${dropped} asked for did not come back` : ""}.`);
49842
+ if (typeof data.note === "string" && data.note.trim()) lines.push(data.note.trim());
49843
+ const numbered = (status) => entries.map(({ lookAlike }, index) => lookAlike.status === status ? index + 1 : 0).filter((n) => n > 0);
49844
+ const matched = numbered("failed");
49845
+ const undecided = numbered("undecided");
49846
+ const chosen = matched.length > 0 ? matched : undecided.length > 0 ? numbered("passed") : [];
49847
+ const pick = chosen.length > 0 ? ` --pick ${chosen.join(",")}` : "";
49848
+ const body = isRecord$1(request.body) ? request.body : {};
49849
+ const ruleId = typeof body.rule_id === "string" ? body.rule_id : null;
49850
+ const requestId = typeof body.request_id === "string" ? body.request_id : null;
49851
+ const target = typeof file === "string" ? shellWord(file) : "<file>";
49852
+ 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`;
49853
+ const one = (count, single, many) => count === 1 ? single : many;
49854
+ 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.` : "";
49855
+ const which = matched.length > 0 ? `Keep ${one(matched.length, "the one it matched as an email", "the ones it matched as emails")} it must not match` : chosen.length > 0 ? `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)` : `Keep ${one(entries.length, "it as an email", "them as emails")} the rule must not match`;
49856
+ const test = matched.length > 0 ? ` Test an edited rule on ${one(matched.length, "it", "them")} with \`primitive email-rules test-email-rule ${ruleId ? `--rule-id ${ruleId}` : "--body-file <body.json>"} --${EXPECT_SAMPLES_FILE_FLAG} ${target}${pick}\`.` : "";
49857
+ if (typeof file === "string") {
49858
+ const samples = entries.map((entry) => entry.sample);
49859
+ if (samples.length === 0) {
49860
+ const message = `Did not write --${LOOK_ALIKES_SAMPLES_FILE_FLAG} ${file}: no look-alikes came back, so it was left as it was.`;
49861
+ return {
49862
+ lines: [...lines, message],
49863
+ exitCode: 1,
49864
+ error: {
49865
+ code: "samples_file_not_written",
49866
+ message
49867
+ }
49868
+ };
49869
+ }
49870
+ try {
49871
+ writeFileSync(file, `${JSON.stringify(samples, null, 2)}\n`);
49872
+ } catch (error) {
49873
+ const message = `Could not write --${LOOK_ALIKES_SAMPLES_FILE_FLAG} ${file}: ${errorDetail(error)}`;
49874
+ return {
49875
+ lines: [
49876
+ ...lines,
49877
+ message,
49878
+ "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."
49879
+ ],
49880
+ exitCode: 1,
49881
+ error: {
49882
+ code: "samples_file_not_written",
49883
+ message
49884
+ }
49885
+ };
49886
+ }
49887
+ const wrote = `Wrote ${one(samples.length, "the look-alike", `the ${samples.length} look-alikes`)} to ${file}, numbered as above.`;
49888
+ lines.push(noneDecided ? `${wrote} ${noneDecided}` : `${wrote} ${which} with ${save}; --pick takes any of the numbers.${test}`);
49889
+ } else if (noneDecided) lines.push(noneDecided);
49890
+ 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}`);
49891
+ return { lines };
49892
+ }
47838
49893
  // src/oclif/email-rule-authoring.ts
47839
49894
  const EMAIL_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
47840
49895
  /** Whether `id` has the shape of an email id (a UUID). */
@@ -47847,9 +49902,6 @@ function usageError(message) {
47847
49902
  function isRecord(value) {
47848
49903
  return value !== null && typeof value === "object" && !Array.isArray(value);
47849
49904
  }
47850
- function errorDetail(error) {
47851
- return error instanceof Error ? error.message : String(error);
47852
- }
47853
49905
  /** `count` and the word for it: "1 expectation", "2 expectations". */
47854
49906
  function plural$1(count, one, many) {
47855
49907
  return `${count} ${count === 1 ? one : many}`;
@@ -47890,7 +49942,7 @@ function checkExpectations(value, label) {
47890
49942
  if (!isRecord(value)) throw usageError(`${label} must hold a JSON object such as {"match": ["<email-id>"], "no_match": ["<email-id>"]}.`);
47891
49943
  const expectations = {};
47892
49944
  for (const [key, list] of Object.entries(value)) {
47893
- if (key !== "match" && key !== "no_match") throw usageError(`${label} takes only "match" and "no_match"; "${key}" is not one of them.`);
49945
+ 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.`);
47894
49946
  if (!Array.isArray(list) || list.some((id) => typeof id !== "string")) throw usageError(`${label}: "${key}" must be a list of email ids.`);
47895
49947
  const ids = [];
47896
49948
  const seen = /* @__PURE__ */ new Set();
@@ -47906,33 +49958,19 @@ function checkExpectations(value, label) {
47906
49958
  assertDisjoint(expectations, label);
47907
49959
  return expectations;
47908
49960
  }
47909
- /** Read and parse a JSON file a flag names, failing like --body-file does. */
47910
- function readJsonFileFlag(path, flag) {
47911
- let text;
47912
- try {
47913
- text = readFileSync(path, "utf8");
47914
- } catch (error) {
47915
- throw new Errors.CLIError(`Could not read ${flag} ${path}: ${errorDetail(error)}`, { exit: 1 });
47916
- }
47917
- try {
47918
- return JSON.parse(text);
47919
- } catch (error) {
47920
- throw new Errors.CLIError(`${flag} ${path} is not valid JSON: ${errorDetail(error)}`, { exit: 1 });
47921
- }
47922
- }
47923
- /** `expect` for test-email-rule, from --expect-file or the id flags. */
49961
+ /**
49962
+ * `expect` for test-email-rule, from --expect-file or the id flags, with the
49963
+ * made-up emails --expect-samples-file holds in expect.samples.
49964
+ */
47924
49965
  function expectBodyFields(flags) {
49966
+ const samples = expectSamples(flags);
47925
49967
  const file = flags["expect-file"];
47926
- if (typeof file === "string") {
47927
- const label = `--expect-file ${file}`;
47928
- return [{
47929
- flag: "--expect-file",
47930
- property: "expect",
47931
- value: checkExpectations(readJsonFileFlag(file, "--expect-file"), label)
47932
- }];
47933
- }
47934
49968
  const expect = {};
47935
49969
  const given = [];
49970
+ if (typeof file === "string") {
49971
+ Object.assign(expect, checkExpectations(readJsonFileFlag(file, "--expect-file"), `--expect-file ${file}`));
49972
+ given.push("--expect-file");
49973
+ }
47936
49974
  const match = flags["expect-match"];
47937
49975
  if (Array.isArray(match)) {
47938
49976
  expect.match = parseEmailIdList(match, "--expect-match");
@@ -47943,8 +49981,12 @@ function expectBodyFields(flags) {
47943
49981
  expect.no_match = parseEmailIdList(noMatch, "--expect-no-match");
47944
49982
  given.push("--expect-no-match");
47945
49983
  }
47946
- if (given.length === 0) return [];
47947
49984
  assertDisjoint(expect, given.join(" and "));
49985
+ if (samples) {
49986
+ expect.samples = samples;
49987
+ given.push(`--${EXPECT_SAMPLES_FILE_FLAG}`);
49988
+ }
49989
+ if (given.length === 0) return [];
47948
49990
  return [{
47949
49991
  flag: given.join(", "),
47950
49992
  property: "expect",
@@ -48081,18 +50123,72 @@ function expectationLine(result, results) {
48081
50123
  const note = result.status === "undecided" ? undecidedNote(undecidedReason(result, results)) : void 0;
48082
50124
  return ` ${result.status}: ${result.email_id} expected ${result.expected}, got ${result.outcome}${note ? ` (${note})` : ""}`;
48083
50125
  }
48084
- function expectationsLines(expectations, testResults) {
50126
+ /** The made-up emails a test's expectations judged, in order. */
50127
+ function sampleJudgments(expectations) {
50128
+ return (isRecord(expectations) && Array.isArray(expectations.samples) ? expectations.samples : []).filter(isRecord).map((item, position) => ({
50129
+ index: typeof item.index === "number" ? item.index : position,
50130
+ label: typeof item.label === "string" ? item.label : null,
50131
+ expected: String(item.expected ?? ""),
50132
+ outcome: String(item.outcome ?? "not_decided"),
50133
+ status: String(item.status ?? "undecided"),
50134
+ question_answers: Array.isArray(item.question_answers) ? item.question_answers.filter(isRecord) : []
50135
+ }));
50136
+ }
50137
+ function sampleNamer(request) {
50138
+ const sent = isRecord(request?.body) && isRecord(request.body.expect) && Array.isArray(request.body.expect.samples);
50139
+ const fromFile = sent && typeof request?.flags?.["expect-samples-file"] === "string";
50140
+ return ({ index, label }) => {
50141
+ const n = fromFile ? sentSampleNumber(index, request?.flags) : index + 1;
50142
+ const name = sent ? `sample ${n}` : `saved sample ${n}`;
50143
+ return label ? `${name} ("${label}")` : name;
50144
+ };
50145
+ }
50146
+ /**
50147
+ * Why a made-up email is undecided, and whether a later test can decide it:
50148
+ * yes when it was not reached or a question went unanswered; maybe not
50149
+ * otherwise, since a spam score it does not carry, a fact its
50150
+ * classification left out or a failed classification look the same. A
50151
+ * spam score left unset makes one undecided even with a question
50152
+ * unanswered, which then reads as decidable later: the answer cannot tell.
50153
+ */
50154
+ function sampleUndecidedReason(sample) {
50155
+ if (sample.outcome === "not_decided") return {
50156
+ why: "it was not reached in time, or deciding it failed",
50157
+ resolvable: true
50158
+ };
50159
+ const answers = sample.question_answers;
50160
+ if (answers.some((answer) => answer.answer === null) && !answers.some((answer) => answer.answer === false)) return {
50161
+ why: "a question went unanswered",
50162
+ resolvable: true
50163
+ };
50164
+ return {
50165
+ why: "it reads a spam score the sample does not carry, a fact its classification left out, or its classification failed",
50166
+ resolvable: false,
50167
+ may: true
50168
+ };
50169
+ }
50170
+ function sampleLines(sample, name) {
50171
+ const note = sample.status === "undecided" ? ` (${undecidedNote(sampleUndecidedReason(sample))})` : "";
50172
+ const lines = [` ${sample.status}: ${name(sample)} expected ${sample.expected}, got ${sample.outcome}${note}`];
50173
+ if (sample.question_answers.length > 0) lines.push(` questions: ${sample.question_answers.map(answerText).join("; ")}`);
50174
+ return lines;
50175
+ }
50176
+ function expectationsLines(expectations, testResults, name = sampleNamer()) {
48085
50177
  if (!expectations) return [];
48086
50178
  const results = Array.isArray(expectations.results) ? expectations.results : [];
48087
50179
  const passed = expectations.counts?.passed ?? 0;
48088
50180
  const failed = expectations.counts?.failed ?? 0;
48089
50181
  const undecided = expectations.counts?.undecided ?? 0;
48090
50182
  if (expectations.passed && failed === 0 && undecided === 0) return [`Expectations: all ${passed} passed.`];
48091
- return [`Expectations: ${[
48092
- failed > 0 ? `${failed} failed` : "",
48093
- undecided > 0 ? `${undecided} undecided` : "",
48094
- `${passed} passed`
48095
- ].filter((part) => part !== "").join(", ")}.`, ...results.filter((result) => result.status !== "passed").map((result) => expectationLine(result, testResults))];
50183
+ return [
50184
+ `Expectations: ${[
50185
+ failed > 0 ? `${failed} failed` : "",
50186
+ undecided > 0 ? `${undecided} undecided` : "",
50187
+ `${passed} passed`
50188
+ ].filter((part) => part !== "").join(", ")}.`,
50189
+ ...results.filter((result) => result.status !== "passed").map((result) => expectationLine(result, testResults)),
50190
+ ...sampleJudgments(expectations).filter((sample) => sample.status !== "passed").flatMap((sample) => sampleLines(sample, name))
50191
+ ];
48096
50192
  }
48097
50193
  const NOT_MATCHED_ORDER = {
48098
50194
  near_miss: 1,
@@ -48154,14 +50250,24 @@ function notMatchedLines(results, expectations) {
48154
50250
  * test can decide it;
48155
50251
  * - else nothing: the exit code stays 0.
48156
50252
  */
48157
- function expectationsOutcome(expectations, testResults = []) {
50253
+ function expectationsOutcome(expectations, testResults = [], name = sampleNamer()) {
48158
50254
  if (!expectations) return {};
48159
50255
  const results = Array.isArray(expectations.results) ? expectations.results : [];
48160
- const failed = results.filter((result) => result.status === "failed").map((result) => result.email_id);
48161
- const undecided = results.filter((result) => result.status === "undecided").map((result) => ({
50256
+ const samples = sampleJudgments(expectations);
50257
+ const failed = [...results.filter((result) => result.status === "failed").map((result) => result.email_id), ...samples.filter((sample) => sample.status === "failed").map((sample) => name({
50258
+ index: sample.index,
50259
+ label: null
50260
+ }))];
50261
+ const undecided = [...results.filter((result) => result.status === "undecided").map((result) => ({
48162
50262
  id: result.email_id,
48163
50263
  reason: undecidedReason(result, testResults)
48164
- }));
50264
+ })), ...samples.filter((sample) => sample.status === "undecided").map((sample) => ({
50265
+ id: name({
50266
+ index: sample.index,
50267
+ label: null
50268
+ }),
50269
+ reason: sampleUndecidedReason(sample)
50270
+ }))];
48165
50271
  const unresolvable = undecided.filter(({ reason }) => !reason.resolvable);
48166
50272
  const failedCount = Math.max(expectations.counts?.failed ?? 0, failed.length);
48167
50273
  const undecidedCount = Math.max(expectations.counts?.undecided ?? 0, undecided.length);
@@ -48177,7 +50283,7 @@ function expectationsOutcome(expectations, testResults = []) {
48177
50283
  exitCode: 1,
48178
50284
  error: {
48179
50285
  code: "expectations_unresolvable",
48180
- message: `${plural$1(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again will not decide ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
50286
+ 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)}.`
48181
50287
  }
48182
50288
  };
48183
50289
  if (undecidedCount > 0) return {
@@ -48190,8 +50296,9 @@ function expectationsOutcome(expectations, testResults = []) {
48190
50296
  return {};
48191
50297
  }
48192
50298
  /** The stderr report and exit code of a test-email-rule answer. */
48193
- function testEmailRuleReport(data) {
50299
+ function testEmailRuleReport(data, request) {
48194
50300
  if (!isRecord(data)) return { lines: [] };
50301
+ const name = sampleNamer(request);
48195
50302
  const result = data;
48196
50303
  const results = Array.isArray(result.results) ? result.results : [];
48197
50304
  const summary = isRecord(result.summary) ? result.summary : void 0;
@@ -48201,24 +50308,58 @@ function testEmailRuleReport(data) {
48201
50308
  ...scopeWarningLines(result.scope_warnings),
48202
50309
  ...conditionLines(summary),
48203
50310
  ...questionLines(summary, results),
48204
- ...expectationsLines(result.expectations, results),
50311
+ ...expectationsLines(result.expectations, results, name),
48205
50312
  ...notMatchedLines(results, result.expectations)
48206
50313
  ],
48207
- ...expectationsOutcome(result.expectations, results)
50314
+ ...expectationsOutcome(result.expectations, results, name)
50315
+ };
50316
+ }
50317
+ /** A list of numbers from 0, as the API gives sample indexes. */
50318
+ function indexList(value) {
50319
+ return Array.isArray(value) ? value.filter((n) => typeof n === "number") : [];
50320
+ }
50321
+ /**
50322
+ * How a rule write names a made-up email its check gives by index, from 0 in
50323
+ * the set judged: those this write sends in place of the rule's
50324
+ * (expectation_samples), or the rule's saved ones oldest first and then
50325
+ * those it adds (add_expectation_samples). One this write sent is "sample
50326
+ * N", its number in the samples file --pick took it from (or its place in
50327
+ * what was sent); any other is "saved sample N", its place among the rule's.
50328
+ * With its label when known. `saved` is the answer's expectation_samples,
50329
+ * every one the rule keeps after the write, when it has them.
50330
+ */
50331
+ function writtenSampleNamer(body, flags, saved) {
50332
+ const sent = isRecord(body) && Array.isArray(body.expectation_samples) ? body.expectation_samples : null;
50333
+ const added = isRecord(body) && Array.isArray(body.add_expectation_samples) ? body.add_expectation_samples : null;
50334
+ const kept = Array.isArray(saved) ? saved : null;
50335
+ const labelled = (name, sample) => isRecord(sample) && typeof sample.label === "string" && sample.label ? `${name} ("${sample.label}")` : name;
50336
+ return (index) => {
50337
+ if (sent) return labelled(`sample ${sentSampleNumber(index, flags)}`, sent[index]);
50338
+ if (added) {
50339
+ const start = kept ? kept.length - added.length : null;
50340
+ if (start === null) return `made-up email ${index + 1}`;
50341
+ if (index >= start) return labelled(`sample ${sentSampleNumber(index - start, flags)}`, added[index - start]);
50342
+ }
50343
+ return labelled(`saved sample ${index + 1}`, kept?.[index]);
48208
50344
  };
48209
50345
  }
48210
- function expectationsCheckLine(check, ruleId) {
50346
+ function expectationsCheckLine(check, ruleId, name = (index) => `saved sample ${index + 1}`) {
48211
50347
  if (!isRecord(check)) return void 0;
48212
50348
  const on = typeof check.version === "number" ? `version ${check.version}` : "the current version";
48213
50349
  const failed = Array.isArray(check.failed) ? check.failed : [];
48214
50350
  const undecided = Array.isArray(check.undecided) ? check.undecided : [];
48215
- if (check.passed && failed.length === 0 && undecided.length === 0) return `Expectations on ${on}: all passed.`;
50351
+ const failedSamples = indexList(check.failed_samples);
50352
+ const undecidedSamples = indexList(check.undecided_samples);
50353
+ if (check.passed && failed.length === 0 && undecided.length === 0 && failedSamples.length === 0 && undecidedSamples.length === 0) return `Expectations on ${on}: all passed.`;
48216
50354
  const parts = [];
48217
50355
  if (failed.length > 0) parts.push(`${failed.length} failed (${failed.join(", ")})`);
48218
- else if (undecided.length > 0) parts.push("none failed");
50356
+ if (failedSamples.length > 0) parts.push(`${plural$1(failedSamples.length, "made-up email", "made-up emails")} failed (${failedSamples.map(name).join(", ")})`);
50357
+ if (failed.length === 0 && failedSamples.length === 0 && (undecided.length > 0 || undecidedSamples.length > 0)) parts.push("none failed");
48219
50358
  if (undecided.length > 0) parts.push(`${undecided.length} undecided (${undecided.join(", ")})`);
48220
- const rest = undecided.length > 0 ? ` To decide the rest, run \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.` : "";
48221
- return `Expectations on ${on}: ${parts.length > 0 ? parts.join(", ") : "not passed"}.${rest}`;
50359
+ 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(", ")})`);
50360
+ const rest = undecided.length > 0 || undecidedSamples.length > 0 ? ` To decide the rest, run \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.` : "";
50361
+ const note = typeof check.note === "string" && check.note.trim() ? ` ${check.note.trim()}` : "";
50362
+ return `Expectations on ${on}: ${parts.length > 0 ? parts.join(", ") : "not passed"}.${rest}${note}`;
48222
50363
  }
48223
50364
  /** The flag that makes a rule write's exit code follow its expectations_check. */
48224
50365
  const FAIL_ON_EXPECTATIONS_FLAG = "fail-on-expectations";
@@ -48230,7 +50371,7 @@ const FAIL_ON_EXPECTATIONS_FLAG = "fail-on-expectations";
48230
50371
  * `passed` as false or as null when nothing failed and something is
48231
50372
  * undecided; any other check that did not pass exits 1.
48232
50373
  */
48233
- function writtenExpectationsOutcome(check, sentExpectations, ruleId) {
50374
+ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "", name = (index) => `saved sample ${index + 1}`) {
48234
50375
  const test = `\`primitive email-rules test-email-rule --rule-id ${ruleId}\``;
48235
50376
  if (!isRecord(check)) {
48236
50377
  if (!sentExpectations) return {};
@@ -48238,52 +50379,115 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId) {
48238
50379
  exitCode: 3,
48239
50380
  error: {
48240
50381
  code: "expectations_unchecked",
48241
- message: `The expectations this write sent were not checked on it. Test them with ${test}.`
50382
+ message: `${saved}The expectations this write sent were not checked on it. Test them with ${test}.`
48242
50383
  }
48243
50384
  };
48244
50385
  }
48245
- const failed = Array.isArray(check.failed) ? check.failed : [];
48246
- const undecided = Array.isArray(check.undecided) ? check.undecided : [];
50386
+ const failed = [...Array.isArray(check.failed) ? check.failed : [], ...indexList(check.failed_samples).map(name)];
50387
+ const undecidedSamples = indexList(check.undecided_samples);
50388
+ const undecided = [...Array.isArray(check.undecided) ? check.undecided : [], ...undecidedSamples.map(name)];
48247
50389
  if (failed.length > 0) return {
48248
50390
  exitCode: 1,
48249
50391
  error: {
48250
50392
  code: "expectations_failed",
48251
- message: `${plural$1(failed.length, "expectation", "expectations")} failed on this write: ${failed.join(", ")}.`
50393
+ message: `${saved}${plural$1(failed.length, "expectation", "expectations")} failed on this write: ${failed.join(", ")}.`
48252
50394
  }
48253
50395
  };
48254
50396
  if (undecided.length > 0) return {
48255
50397
  exitCode: 3,
48256
50398
  error: {
48257
50399
  code: "expectations_undecided",
48258
- message: `${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}.`
50400
+ 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}.`
48259
50401
  }
48260
50402
  };
48261
50403
  if (check.passed !== true) return {
48262
50404
  exitCode: 1,
48263
50405
  error: {
48264
50406
  code: "expectations_failed",
48265
- message: "The write's expectations check did not pass."
50407
+ message: `${saved}The write's expectations check did not pass.`
48266
50408
  }
48267
50409
  };
48268
50410
  return {};
48269
50411
  }
50412
+ /**
50413
+ * update-email-rule's post-save outcome says the write was saved: the server
50414
+ * judges a write before saving only when it saves a version or turns the
50415
+ * rule active, and refuses with these same codes, saving nothing.
50416
+ */
50417
+ 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. ";
50418
+ /** What a written do_not_share rule's report says first. */
50419
+ const BLOCKS_SHARING = "This rule blocks sharing (kind do_not_share): while it is active, no rule shares an email it matches.";
50420
+ /** A do_not_share rule's protection_note, as a line, or undefined. */
50421
+ function protectionLine(rule) {
50422
+ if (!isRecord(rule)) return void 0;
50423
+ const note = rule.protection_note;
50424
+ return typeof note === "string" && note.trim() ? `Protection: ${note.trim()}` : void 0;
50425
+ }
50426
+ /** What a restore_version write saved, as its answer says. */
50427
+ function restoredLine(restored, currentVersion) {
50428
+ if (!isRecord(restored) || typeof restored.version !== "number") return void 0;
50429
+ const note = typeof restored.note === "string" && restored.note.trim() ? ` ${restored.note.trim()}` : "";
50430
+ const as = typeof currentVersion === "number" ? `as version ${currentVersion}, now current` : "as the current version";
50431
+ return `Saved version ${restored.version} again ${as}.${note}`;
50432
+ }
50433
+ /**
50434
+ * Who an update that saved a version reaches, as its answer counts them: the
50435
+ * keys listing the rule, the webhooks those keys own, and the shares the
50436
+ * write ended.
50437
+ */
50438
+ function affectsLine(affects) {
50439
+ if (!isRecord(affects)) return void 0;
50440
+ const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
50441
+ const keys = count(affects.keys);
50442
+ const endpoints = count(affects.endpoints);
50443
+ const shares = count(affects.shares_ended);
50444
+ if (keys === void 0 || endpoints === void 0 || shares === void 0) return void 0;
50445
+ return `This version reaches ${plural$1(keys, "API key", "API keys")} listing the rule and ${plural$1(endpoints, "webhook", "webhooks")} those keys own; it ended ${plural$1(shares, "share", "shares")}. The rule keeps its state.`;
50446
+ }
50447
+ /** The rule's author, by name and email where the answer gives them. */
50448
+ function authorLine(author) {
50449
+ if (!isRecord(author) || typeof author.user_id !== "string") return void 0;
50450
+ const name = typeof author.name === "string" && author.name.trim() ? author.name.trim() : null;
50451
+ const email = typeof author.email === "string" && author.email.trim() ? author.email.trim() : null;
50452
+ if (name && email) return `Author: ${name} <${email}>`;
50453
+ if (name || email) return `Author: ${name ?? email}`;
50454
+ return `Author: user ${author.user_id} (name and email not shown to you)`;
50455
+ }
48270
50456
  /** The stderr report of a rule written by create-email-rule or update-email-rule. */
48271
50457
  function writtenEmailRuleReport(data, request) {
48272
50458
  if (!isRecord(data)) return { lines: [] };
48273
50459
  const rule = data;
48274
50460
  const ruleId = typeof rule.id === "string" ? rule.id : "<rule-id>";
48275
50461
  const lines = scopeWarningLines(rule.scope_warnings);
48276
- 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." : ""}`);
48277
- const sentExpectations = isRecord(request.body) && isRecord(request.body.expectations);
48278
- const checked = expectationsCheckLine(rule.expectations_check, ruleId);
50462
+ if (rule.kind === "do_not_share") {
50463
+ const note = protectionLine(rule);
50464
+ lines.unshift(...note ? [BLOCKS_SHARING, note] : [`${BLOCKS_SHARING}${typeof rule.state === "string" && rule.state !== "active" ? " It blocks nothing until it is active." : ""}`]);
50465
+ }
50466
+ const restored = restoredLine(rule.restored_from, rule.current_version);
50467
+ if (restored) lines.push(restored);
50468
+ const reach = affectsLine(rule.affects);
50469
+ if (reach) lines.push(reach);
50470
+ const sentExpectations = sentExpectationsIn(request.body);
50471
+ const name = writtenSampleNamer(request.body, request.flags, rule.expectation_samples);
50472
+ const checked = expectationsCheckLine(rule.expectations_check, ruleId, name);
48279
50473
  if (checked) lines.push(checked);
48280
50474
  else if (sentExpectations) lines.push(`Expectations were not checked on this write. Test them with \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.`);
48281
50475
  if (request.flags?.["fail-on-expectations"] !== true) return { lines };
48282
50476
  return {
48283
50477
  lines,
48284
- ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId)
50478
+ ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId, isUpdate(request) ? SAVED_UNJUDGED : "", name)
48285
50479
  };
48286
50480
  }
50481
+ /** Whether a rule write sent expectations: emails by id, or made-up emails to save or add. */
50482
+ function sentExpectationsIn(body) {
50483
+ if (!isRecord(body)) return false;
50484
+ const some = (value) => Array.isArray(value) && value.length > 0;
50485
+ return isRecord(body.expectations) || some(body.expectation_samples) || some(body.add_expectation_samples);
50486
+ }
50487
+ /** Whether a rule write is an update: it names the rule in its path. */
50488
+ function isUpdate(request) {
50489
+ return typeof request.path?.id === "string";
50490
+ }
48287
50491
  /**
48288
50492
  * A rule write's exit code when its report cannot be built, under
48289
50493
  * --fail-on-expectations: 1 for a check that is there and did not pass, and 3
@@ -48291,7 +50495,7 @@ function writtenEmailRuleReport(data, request) {
48291
50495
  */
48292
50496
  function writtenFallbackExitCode(data, request) {
48293
50497
  if (request.flags["fail-on-expectations"] !== true) return void 0;
48294
- return writtenExpectationsOutcome(isRecord(data) ? data.expectations_check : void 0, isRecord(request.body) && isRecord(request.body.expectations), "<rule-id>").exitCode;
50498
+ return writtenExpectationsOutcome(isRecord(data) ? data.expectations_check : void 0, sentExpectationsIn(request.body), "<rule-id>").exitCode;
48295
50499
  }
48296
50500
  /** The stderr report of get-email-rule-scope. */
48297
50501
  function emailRuleScopeReport(data) {
@@ -48350,9 +50554,156 @@ function versionMovedHint(payload, ruleId) {
48350
50554
  const read = `Read it again with \`primitive email-rules get-email-rule ${id}\` and reapply your change to what it shows`;
48351
50555
  return current === void 0 ? `Hint: the rule's version is not the if_version you sent, so nothing was saved, and the answer did not give its current version. ${read}.` : `Hint: the rule's current version is ${current}, not the if_version you sent, so nothing was saved. ${read}, then retry with --if-version ${current}.`;
48352
50556
  }
48353
- /** A write's --fail-on-expectations, the same on create and update. */
48354
- 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.";
50557
+ /** The read-again advice every conflict ends with. */
50558
+ function readAgain(id) {
50559
+ return `Read it again with \`primitive email-rules get-email-rule ${id}\` and send the write again`;
50560
+ }
50561
+ /**
50562
+ * The hint for update-email-rule's refusals: an email_rule_conflict by its
50563
+ * details.reason (version_moved, revision_moved, expectations_moved,
50564
+ * rule_changed), and the expectations guard's expectations_failed,
50565
+ * expectations_undecided and no_expectations. Nothing was saved in any of
50566
+ * them.
50567
+ */
50568
+ function updateEmailRuleErrorHint(code, payload, ruleId, body, flags) {
50569
+ const id = typeof ruleId === "string" && ruleId ? ruleId : "<rule-id>";
50570
+ const details = errorDetails(payload);
50571
+ const guarded = isRecord(body) && body.if_expectations_pass === true;
50572
+ if (code === "email_rule_conflict") switch (details.reason) {
50573
+ case "version_moved": return versionMovedHint(payload, ruleId);
50574
+ case "revision_moved": {
50575
+ const current = typeof details.current_revision === "number" ? details.current_revision : void 0;
50576
+ return current === void 0 ? `Hint: the rule was updated since the if_revision you sent, so nothing was saved. ${readAgain(id)} with --if-revision set to its revision.` : `Hint: the rule is now at revision ${current}, not the if_revision you sent, so nothing was saved. ${readAgain(id)}, reapplying your change to what it shows, with --if-revision ${current}.`;
50577
+ }
50578
+ case "expectations_moved": return `Hint: the rule's saved expectations changed while this write was checked against them, so nothing was saved. ${readAgain(id)}.`;
50579
+ case "rule_changed": return guarded ? `Hint: the rule changed after this write was judged against its expectations and before it was saved, so saving it would have gone unchecked; nothing was saved. ${readAgain(id)}.` : `Hint: the rule changed while this write was being saved, so nothing was saved. ${readAgain(id)}.`;
50580
+ default: return;
50581
+ }
50582
+ if (code === "expectations_failed" || code === "expectations_undecided") {
50583
+ const check = isRecord(details.expectations_check) ? details.expectations_check : {};
50584
+ const failed = Array.isArray(check.failed) ? check.failed : [];
50585
+ const undecided = Array.isArray(check.undecided) ? check.undecided : [];
50586
+ const name = writtenSampleNamer(body, flags);
50587
+ const failedAll = [...failed, ...indexList(check.failed_samples).map(name)];
50588
+ const undecidedSamples = indexList(check.undecided_samples);
50589
+ const undecidedAll = [...undecided, ...undecidedSamples.map(name)];
50590
+ const parts = [...failedAll.length > 0 ? [`failed: ${failedAll.join(", ")}`] : [], ...undecidedAll.length > 0 ? [`undecided: ${undecidedAll.join(", ")}`] : []];
50591
+ 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." : "";
50592
+ const which = parts.length > 0 ? ` Expectations ${parts.join("; ")}.${addedOrder}` : "";
50593
+ const reason = String(details.reason);
50594
+ const samplesMayStay = undecidedSamplesMayStay(undecidedSamples.length, reason);
50595
+ const byReason = reason !== "none" && Object.hasOwn(UNDECIDED_NEXT_STEP, reason) ? UNDECIDED_NEXT_STEP[reason] : UNDECIDED_NEXT_STEP.none;
50596
+ 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}.`;
50597
+ }
50598
+ 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).";
50599
+ }
50600
+ /** The reasons a guard's undecided judgment can be decided by sending the write again. */
50601
+ const RETRYABLE_REASONS = /* @__PURE__ */ new Set([
50602
+ "deadline",
50603
+ "error",
50604
+ "client_gone"
50605
+ ]);
50606
+ /**
50607
+ * A made-up email can stay undecided: one that reads a spam score it does
50608
+ * not carry, or whose classification failed or left out a fact the rule
50609
+ * needs, decides the same way every time.
50610
+ */
50611
+ 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.";
50612
+ /**
50613
+ * How to see why a refused write's made-up emails are undecided: only
50614
+ * test-email-rule-on-sample answers with what a made-up email lacked. Test
50615
+ * the tree this write sent (or the version it restores), else the rule, on
50616
+ * each. Nothing was saved, so a test by rule id would see neither.
50617
+ */
50618
+ function refusedSampleTest(body, ruleId) {
50619
+ const onSample = "`primitive email-rules test-email-rule-on-sample`";
50620
+ if (isRecord(body) && isRecord(body.tree)) return `To see why, test the tree this write sent on each with ${onSample}.`;
50621
+ 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}.`;
50622
+ 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)`}.`;
50623
+ }
50624
+ /** What to do about an undecided judgment, by details.reason ("none": no reason given). */
50625
+ const UNDECIDED_NEXT_STEP = {
50626
+ none: "Some could not be decided yet (mail not classified yet, or a question not answered yet); send the write again later.",
50627
+ deadline: "The judgment ran out of time; send the write again.",
50628
+ error: "The judgment could not finish; send the write again.",
50629
+ client_gone: "The request ended before the write was saved; send it again.",
50630
+ author_unresolvable: "The rule cannot run as its author over the mail it would cover (details.author_reason), so sending the write again will not decide them."
50631
+ };
50632
+ /**
50633
+ * update-email-rule's exit code for the expectations guard's refusals: 1 when
50634
+ * one failed, or when they may not be decided by trying again (the rule
50635
+ * cannot run as its author, or a made-up email is undecided for no reason
50636
+ * the answer gives); 3 when trying again later can decide them.
50637
+ */
50638
+ function updateEmailRuleErrorExitCode(payload) {
50639
+ const code = isRecord(payload) ? payload.code : void 0;
50640
+ if (code === "expectations_failed") return 1;
50641
+ if (code !== "expectations_undecided") return void 0;
50642
+ const details = errorDetails(payload);
50643
+ if (details.reason === "author_unresolvable") return 1;
50644
+ return undecidedSamplesMayStay(indexList((isRecord(details.expectations_check) ? details.expectations_check : {}).undecided_samples).length, String(details.reason)) ? 1 : 3;
50645
+ }
50646
+ /**
50647
+ * Whether a guard's undecided made-up emails may stay undecided: some are,
50648
+ * and the answer gives no reason that sending the write again can change
50649
+ * (author_unresolvable has its own advice and exit code).
50650
+ */
50651
+ function undecidedSamplesMayStay(count, reason) {
50652
+ return count > 0 && !RETRYABLE_REASONS.has(reason) && reason !== "author_unresolvable";
50653
+ }
50654
+ /** The stderr report of compile-email-rule: how many recent emails it holds for. */
50655
+ function compileEmailRuleReport(data) {
50656
+ if (!isRecord(data)) return { lines: [] };
50657
+ const lines = (Array.isArray(data.warnings) ? data.warnings : []).filter((warning) => typeof warning === "string" && warning.trim() !== "").map((warning) => `Warning: ${warning.trim()}`);
50658
+ const breadth = data.breadth;
50659
+ 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.`);
50660
+ return { lines };
50661
+ }
50662
+ /** The stderr report of get-email-rule: who wrote it, and its revision. */
50663
+ function getEmailRuleReport(data) {
50664
+ if (!isRecord(data)) return { lines: [] };
50665
+ const lines = [];
50666
+ const author = authorLine(data.author);
50667
+ if (author) lines.push(author);
50668
+ const protection = protectionLine(data);
50669
+ if (protection) lines.push(protection);
50670
+ 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.`);
50671
+ return { lines };
50672
+ }
50673
+ /** The stderr report of list-email-rules: the protective rules that protect nothing yet, or less than they seem. */
50674
+ function listEmailRulesReport(data) {
50675
+ const rules = Array.isArray(data) ? data : [];
50676
+ const lines = [];
50677
+ for (const rule of rules) {
50678
+ if (!isRecord(rule)) continue;
50679
+ const note = typeof rule.protection_note === "string" ? rule.protection_note.trim() : "";
50680
+ if (!note) continue;
50681
+ const id = typeof rule.id === "string" ? rule.id : "<rule-id>";
50682
+ const name = typeof rule.name === "string" ? rule.name : "no name";
50683
+ lines.push(`Protection of ${id} (${name}): ${note}`);
50684
+ }
50685
+ return { lines };
50686
+ }
50687
+ /** create-email-rule's --fail-on-expectations: the exit code follows the check made after saving. */
50688
+ 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.";
50689
+ /** update-email-rule's --fail-on-expectations: the server refuses the write. */
50690
+ 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 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.";
50691
+ /** What a samples file flag reads. */
50692
+ 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.";
48355
50693
  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.";
50694
+ /**
50695
+ * update-email-rule's --fail-on-expectations sends if_expectations_pass, over
50696
+ * any in the body. Asking for it while turning it off is refused.
50697
+ */
50698
+ function expectationsGuardBodyFields(flags) {
50699
+ if (flags["fail-on-expectations"] !== true) return [];
50700
+ if (flags["if-expectations-pass"] === false) throw usageError("--fail-on-expectations asks the server to judge the write before saving it; it cannot be combined with --no-if-expectations-pass.");
50701
+ return [{
50702
+ flag: `--${FAIL_ON_EXPECTATIONS_FLAG}`,
50703
+ property: "if_expectations_pass",
50704
+ value: true
50705
+ }];
50706
+ }
48356
50707
  const EMAIL_RULE_OPERATION_EXTENSIONS = {
48357
50708
  testEmailRule: {
48358
50709
  flags: {
@@ -48367,9 +50718,11 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
48367
50718
  exclusive: ["expect-file"]
48368
50719
  }),
48369
50720
  "expect-file": Flags.string({
48370
- 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.",
50721
+ 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.",
48371
50722
  exclusive: ["expect-match", "expect-no-match"]
48372
- })
50723
+ }),
50724
+ [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}` }),
50725
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([EXPECT_SAMPLES_FILE_FLAG]) })
48373
50726
  },
48374
50727
  bodyFields: expectBodyFields,
48375
50728
  bodyProperties: ["expect"],
@@ -48382,23 +50735,67 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
48382
50735
  createEmailRule: {
48383
50736
  flags: {
48384
50737
  "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." }),
50738
+ [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.` }),
50739
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([SAMPLES_FILE_FLAG]) }),
48385
50740
  [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: FAIL_ON_EXPECTATIONS_HELP })
48386
50741
  },
48387
- bodyFields: expectationsBodyFields(false),
48388
- bodyProperties: ["expectations"],
50742
+ bodyFields: (flags) => [...expectationsBodyFields(false)(flags), ...samplesBodyFields(false)(flags)],
50743
+ bodyProperties: ["expectations", "expectation_samples"],
48389
50744
  fallbackExitCode: writtenFallbackExitCode,
48390
50745
  report: writtenEmailRuleReport
48391
50746
  },
48392
50747
  updateEmailRule: {
48393
50748
  flags: {
48394
50749
  "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." }),
48395
- [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: FAIL_ON_EXPECTATIONS_HELP })
50750
+ [SAMPLES_FILE_FLAG]: Flags.string({
50751
+ 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.`,
50752
+ exclusive: [ADD_SAMPLES_FILE_FLAG]
50753
+ }),
50754
+ [ADD_SAMPLES_FILE_FLAG]: Flags.string({
50755
+ 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.`,
50756
+ exclusive: [SAMPLES_FILE_FLAG]
50757
+ }),
50758
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([SAMPLES_FILE_FLAG, ADD_SAMPLES_FILE_FLAG]) }),
50759
+ [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: UPDATE_FAIL_ON_EXPECTATIONS_HELP })
48396
50760
  },
48397
- bodyFields: expectationsBodyFields(true),
48398
- bodyProperties: ["expectations"],
50761
+ bodyFields: (flags) => [
50762
+ ...expectationsBodyFields(true)(flags),
50763
+ ...samplesBodyFields(true)(flags),
50764
+ ...expectationsGuardBodyFields(flags)
50765
+ ],
50766
+ bodyProperties: [
50767
+ "expectations",
50768
+ "expectation_samples",
50769
+ "add_expectation_samples"
50770
+ ],
50771
+ errorExitCode: updateEmailRuleErrorExitCode,
48399
50772
  fallbackExitCode: writtenFallbackExitCode,
48400
50773
  report: writtenEmailRuleReport
48401
50774
  },
50775
+ generateLookAlikes: {
50776
+ 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 not 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.` }) },
50777
+ report: lookAlikesReport
50778
+ },
50779
+ acceptEmailRuleRequest: {
50780
+ flags: {
50781
+ [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 it must never match.` }),
50782
+ [PICK_FLAG]: Flags.string({ description: pickFlagHelp([SAMPLES_FILE_FLAG]) })
50783
+ },
50784
+ bodyFields: samplesBodyFields(false),
50785
+ bodyProperties: ["expectation_samples"]
50786
+ },
50787
+ compileEmailRule: {
50788
+ flags: {},
50789
+ report: compileEmailRuleReport
50790
+ },
50791
+ getEmailRule: {
50792
+ flags: {},
50793
+ report: getEmailRuleReport
50794
+ },
50795
+ listEmailRules: {
50796
+ flags: {},
50797
+ report: listEmailRulesReport
50798
+ },
48402
50799
  getEmailRuleScope: {
48403
50800
  flags: {},
48404
50801
  report: emailRuleScopeReport
@@ -49625,7 +52022,6 @@ function serializeErrorPayload(payload) {
49625
52022
  * outcome is unknown.
49626
52023
  */
49627
52024
  function buildThrownSendFailureEnvelope(params) {
49628
- const message = params.error instanceof Error ? params.error.message : String(params.error);
49629
52025
  const outcome = params.requestStarted ? "uncertain" : "not_sent";
49630
52026
  return {
49631
52027
  outcome,
@@ -49637,7 +52033,7 @@ function buildThrownSendFailureEnvelope(params) {
49637
52033
  }),
49638
52034
  sent: null,
49639
52035
  http_status: null,
49640
- error: { message },
52036
+ error: thrownErrorBody(params.error),
49641
52037
  follow_up_commands: params.requestStarted && params.attemptStartedAtIso ? buildSendCommandFollowUps({
49642
52038
  attemptStartedAtIso: params.attemptStartedAtIso,
49643
52039
  idempotencyKey: params.idempotencyKey,
@@ -50530,7 +52926,7 @@ function savedConnectedProfiles(configDir, exclude) {
50530
52926
  */
50531
52927
  async function otherProfiles(configDir, session, current) {
50532
52928
  if (session) try {
50533
- const { connectedProfilesForSession } = await import("./machine-session-3H-rNH8v.js").then((n) => n.s);
52929
+ const { connectedProfilesForSession } = await import("./machine-session-DNnNhhFr.js").then((n) => n.s);
50534
52930
  return {
50535
52931
  inSession: true,
50536
52932
  profiles: connectedProfilesForSession(configDir, session, current)
@@ -51637,7 +54033,7 @@ const OPERATION_ERROR_HINTS = {
51637
54033
  inviteOrganizationMember: (code, body, payload) => inviteRefusalHint(code, body, payload),
51638
54034
  createEndpoint: (code, body, payload) => createEndpointRuleRequestHint(code, body, payload),
51639
54035
  updateEndpoint: (code, body, payload) => updateEndpointRuleRequestHint(code, body, payload),
51640
- updateEmailRule: (code, _body, payload, path) => code === "email_rule_conflict" ? versionMovedHint(payload, path?.id) : void 0,
54036
+ updateEmailRule: (code, body, payload, path, flags) => updateEmailRuleErrorHint(code, payload, path?.id, body, flags),
51641
54037
  verifyDomain: (code, _body, payload) => code === "dns_verification_failed" ? domainVerificationFailedHint(payload) : void 0,
51642
54038
  addDomain: (code, body) => {
51643
54039
  if (code === "mail_relay_unavailable") return MAIL_RELAY_UNAVAILABLE_MESSAGE;
@@ -51840,7 +54236,7 @@ function createOperationCommand(operation) {
51840
54236
  }) : await callOperation();
51841
54237
  if (result.error) {
51842
54238
  const errorPayload = extractErrorPayload(result.error);
51843
- const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload, collectValues(operation.pathParams, parsedFlags, operation.sdkName));
54239
+ const operationErrorHint = OPERATION_ERROR_HINTS[operation.sdkName]?.(extractErrorCode(errorPayload), body, errorPayload, collectValues(operation.pathParams, parsedFlags, operation.sdkName), parsedFlags);
51844
54240
  writeErrorWithHints(errorPayload, result.response, { codeHint: !(operationErrorHint && OPERATION_ERROR_HINTS_REPLACING_CODE_HINT.has(operation.sdkName)) });
51845
54241
  if (operationErrorHint) process.stderr.write(`${operationErrorHint}\n`);
51846
54242
  if (stepUpUrl) process.stderr.write(approveThenRerunHint(stepUpUrl));
@@ -51867,7 +54263,7 @@ function createOperationCommand(operation) {
51867
54263
  }
51868
54264
  });
51869
54265
  if (operation.sdkName === "getEmail" && extractErrorCode(errorPayload) === "not_found" && typeof parsedFlags.id === "string") await explainPendingMailNotFound(this.config.configDir, parsedFlags.id);
51870
- process.exitCode = 1;
54266
+ process.exitCode = extension?.errorExitCode?.(errorPayload) || 1;
51871
54267
  return;
51872
54268
  }
51873
54269
  if (operation.binaryResponse) {
@@ -52005,4 +54401,4 @@ const EMPTY_RESULT_HINTS = {
52005
54401
  function canonicalizeCliReferences(description) {
52006
54402
  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'`");
52007
54403
  }
52008
- export { replyExpectation as $, createCliApiClient as $t, bareAddress$1 as A, connectNextSteps as At, wakeReadCommand as B, withFlagSuggestion as Bt, terminalApprovalIo as C, sendWaitClampNotice as Ct, listenForCopyHotkey as D, thrownErrorExitCode as Dt, formatBrowserLoginInstructions as E, successfulSendOutcome as Et, readThreadContext as F, currentMailSessionKey as Ft, clearReadPendingMail as G, isEmailId as Gt, wakeRelationship as H, readCallerMailFor as Ht, sentInThread as I, detectAgentRuntime as It, recordPendingMail as J, ATTACHMENT_WAIT_MAX_MS as Jt, pendingMailPath as K, noFactsMessage as Kt, serverMuted as L, detectRuntimeSession as Lt, displayAddress as M, generatedAgentName as Mt, formatMailWakeLine as N, identitySuggestions as Nt, openBrowser as O, DEFAULT_ENROLL_NAME as Ot, latestOwnOutbound as P, loadSkillLine as Pt, readEmailInteraction as Q, createAuthenticatedCliApiClient as Qt, serverRelationship as R, runtimeSessionVariableList as Rt, promptLine as S, sendOutcomeExitCode as St, canOfferCopyHotkey as T, serializeErrorPayload as Tt, claimPendingMail as U, classificationFactLines as Ut, wakeRecipientField as V, emptyEmailListHint as Vt, clearConsumedPendingMail as W, classificationOf as Wt, interactionHumanLine as X, ConnectedAgentKeyRequiredError as Xt, removePendingMail as Y, withAttachmentWait as Yt, interactionNextActions as Z, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as Zt, writeErrorWithHints as _, formatSendFailureSummary as _t, createOperationCommand as a, RESTRICTED_ROLE_NOTE as an, apiResultHttpStatus as at, STEP_UP_TOKEN_FLAG_DESCRIPTION as b, reportSendCommandResult as bt, formatErrorPayload as c, isBuiltInRoleKey as cn, buildThrownSendFailureEnvelope as ct, isGrantRefusal as d, roleKeyText as dn, deriveSendIdempotencyKey as dt, refreshStoredCliCredentials as en, replyInteractionWarning as et, parameterFlagName as f, switchCurrentOrg as fn, excludeSignalReplies as ft, takesStepUp as g, operationManifest as gn, formatPriorRepliesWarning as gt, surfaceUnauthorizedHint as h, setKnownCommands as hn, formatPriorRepliesCheckSkipped as ht, TIME_FLAG_DESCRIPTION as i, CliOrgRefusedError as in, SEND_OUTCOME_HELP as it, describeWake as j, defaultConnectionName as jt, explainPendingMailNotFound as k, cliInvocation as kt, hasControlCharacter as l, revokeUserLogin as ln, checkPriorReplies as lt, runWithTiming as m, sanitizeForTerminal as mn, formatDeletedEarlierSendNotice as mt, API_ERROR_CODES as n, resolveCliApiRequestConfig as nn, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as nt, extractErrorCode as o, credentialsFromUserLogin as on, assertValidIdempotencyKey as ot, readTextFileFlag as p, routeWarningLines as pn, followUpCommandPrefix as pt, readPendingMail as q, ATTACHMENT_WAIT_FLAG_DESCRIPTION as qt, OPERATION_PATH_FLAGS as r, selectedOrgFromEnv as rn, MAX_SEND_WAIT_TIMEOUT_MS as rt, extractErrorPayload as s, fetchCliOrgs as sn, buildFollowUpCommand as st, API_BASE_URL_FLAG_DESCRIPTION as t, refuseOrgSelectionForProfile as tn, wakeInteractionSentence as tt, headerFlagName as u, roleKeyLabel as un, classifySendError as ut, BROWSER_FLAG_DESCRIPTION as v, formatSendRecordFailureSummary as vt, withApproval as w, sentHistoryWindowStart as wt, approveThenRerunHint as x, sendIdentityFields as xt, STEP_UP_HEADER as y, priorRepliesThatWentOut as yt, skillFirstLine as z, formatAlreadySentNotice as zt };
54404
+ export { readEmailInteraction as $, SAVED_CLI_OAUTH_SESSION_EXPIRED_MESSAGE as $t, explainPendingMailNotFound as A, cliInvocation as At, skillFirstLine as B, formatAlreadySentNotice as Bt, promptLine as C, sendOutcomeExitCode as Ct, formatBrowserLoginInstructions as D, successfulSendOutcome as Dt, canOfferCopyHotkey as E, serializeErrorPayload as Et, latestOwnOutbound as F, loadSkillLine as Ft, clearConsumedPendingMail as G, classificationFactLines as Gt, wakeRecipientField as H, emptyEmailListHint as Ht, readThreadContext as I, currentMailSessionKey as It, readPendingMail as J, noFactsMessage as Jt, clearReadPendingMail as K, classificationOf as Kt, sentInThread as L, detectAgentRuntime as Lt, describeWake as M, defaultConnectionName as Mt, displayAddress as N, generatedAgentName as Nt, listenForCopyHotkey as O, thrownErrorExitCode as Ot, formatMailWakeLine as P, identitySuggestions as Pt, interactionNextActions as Q, ConnectedAgentKeyRequiredError as Qt, serverMuted as R, detectRuntimeSession as Rt, approveThenRerunHint as S, sendIdentityFields as St, withApproval as T, sentHistoryWindowStart as Tt, wakeRelationship as U, mailReach as Ut, wakeReadCommand as V, withFlagSuggestion as Vt, claimPendingMail as W, readCallerMailFor as Wt, removePendingMail as X, ATTACHMENT_WAIT_MAX_MS as Xt, recordPendingMail as Y, ATTACHMENT_WAIT_FLAG_DESCRIPTION as Yt, interactionHumanLine as Z, withAttachmentWait as Zt, takesStepUp as _, setKnownCommands as _n, formatPriorRepliesWarning as _t, createOperationCommand as a, selectedOrgFromEnv as an, SEND_OUTCOME_HELP as at, STEP_UP_HEADER as b, priorRepliesThatWentOut as bt, formatErrorPayload as c, credentialsFromUserLogin as cn, buildFollowUpCommand as ct, isGrantRefusal as d, revokeUserLogin as dn, classifySendError as dt, createAuthenticatedCliApiClient as en, replyExpectation as et, parameterFlagName as f, roleKeyLabel as fn, deriveSendIdempotencyKey as ft, surfaceUnauthorizedHint as g, sanitizeForTerminal as gn, formatPriorRepliesCheckSkipped as gt, runWithTiming as h, routeWarningLines as hn, formatDeletedEarlierSendNotice as ht, TIME_FLAG_DESCRIPTION as i, resolveCliApiRequestConfig as in, MAX_SEND_WAIT_TIMEOUT_MS as it, bareAddress$1 as j, connectNextSteps as jt, openBrowser as k, DEFAULT_ENROLL_NAME as kt, hasControlCharacter as l, fetchCliOrgs as ln, buildThrownSendFailureEnvelope as lt, refusalDetails as m, switchCurrentOrg as mn, followUpCommandPrefix as mt, API_ERROR_CODES as n, refreshStoredCliCredentials as nn, wakeInteractionSentence as nt, extractErrorCode as o, CliOrgRefusedError as on, apiResultHttpStatus as ot, readTextFileFlag as p, roleKeyText as pn, excludeSignalReplies as pt, pendingMailPath as q, isEmailId as qt, OPERATION_PATH_FLAGS as r, refuseOrgSelectionForProfile as rn, IDEMPOTENCY_KEY_FLAG_DESCRIPTION as rt, extractErrorPayload as s, RESTRICTED_ROLE_NOTE as sn, assertValidIdempotencyKey as st, API_BASE_URL_FLAG_DESCRIPTION as t, createCliApiClient as tn, replyInteractionWarning as tt, headerFlagName as u, isBuiltInRoleKey as un, checkPriorReplies as ut, writeErrorWithHints as v, operationManifest as vn, formatSendFailureSummary as vt, terminalApprovalIo as w, sendWaitClampNotice as wt, STEP_UP_TOKEN_FLAG_DESCRIPTION as x, reportSendCommandResult as xt, BROWSER_FLAG_DESCRIPTION as y, formatSendRecordFailureSummary as yt, serverRelationship as z, runtimeSessionVariableList as zt };