primcli 3.0.0-preview.7 → 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,4 +1,4 @@
1
- import { B as ensureSearchReportsReplyState, Ht as getEmail, J as SENT_EMAIL_EVENT_TYPES, K as replyStateSurfaceForOperation, L as ReplyStateUnsupportedError, R as assertAwaitingFilterRows, W as isAwaitingRejectedError, Wn as sdk_gen_exports, Y as WEBHOOK_EVENT_TYPES, Zt as getOwnGrant, at as automatedRejectedError, ct as expectedAutomatedVerdicts, gn as listEndpoints, it as assertAutomatedVerdict, ot as automatedSurfaceForOperation, q as EMAIL_RULE_REQUEST_EVENT_TYPES, rt as AutomatedFilterUnsupportedError, st as ensureSearchReportsAutomated, tn as getThread, ut as isAutomatedRejectedError, v as parseRetryAfter, z as awaitingRejectedError } from "./repeats-Dj_o6c-X-C6tOKAqk.js";
1
+ import { B as ensureSearchReportsReplyState, Ht as getEmail, J as SENT_EMAIL_EVENT_TYPES, K as replyStateSurfaceForOperation, L as ReplyStateUnsupportedError, R as assertAwaitingFilterRows, W as isAwaitingRejectedError, Wn as sdk_gen_exports, Y as WEBHOOK_EVENT_TYPES, Zt as getOwnGrant, at as automatedRejectedError, ct as expectedAutomatedVerdicts, gn as listEndpoints, it as assertAutomatedVerdict, ot as automatedSurfaceForOperation, q as EMAIL_RULE_REQUEST_EVENT_TYPES, rt as AutomatedFilterUnsupportedError, st as ensureSearchReportsAutomated, tn as getThread, ut as isAutomatedRejectedError, v as parseRetryAfter, z as awaitingRejectedError } from "./repeats-Dj_o6c-X-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
4
  import { a as thrownErrorBody } from "./json-output-ch756TXV.js";
@@ -10557,7 +10557,7 @@ const operationManifest = [
10557
10557
  "binaryResponse": false,
10558
10558
  "bodyRequired": true,
10559
10559
  "command": "revoke-credential",
10560
- "description": "Revoke one credential by kind and id. A Function key is rotated, never revoked (409 rotate_instead). An API key is revoked only by a person whose own access covers the key's access (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. What the credential set up or owns is listed in standing_objects for review, and for an API key also what it carries on for the keys it was rotated from. An organization-owned object (org_owned true) keeps working; any other keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to, except that a block filter an app made keeps blocking, within the reach the app had, unless the person revoking chose to stop it. An API key (kind org_key or connected_key) that is already revoked, and whose record is kept, answers 200 with already_revoked true and revoked 0, to a caller who could have revoked it; anyone else gets the answer they would get for a key that does not exist. The person's own Primitive CLI login revokes an API key (kind org_key) as the person, with no step-up unless the organization's credential policy sets cli_writes_require_approval (then it takes a keys.create step-up); every other kind (a connected key named as org_key included, 403 grant_forbidden target_not_covered), and every other app even with a step-up, is refused outside the dashboard (403). Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:manage. API keys, agent connection keys and agents are refused.",
10560
+ "description": "Revoke one credential by kind and id. A Function key is rotated, never revoked (409 rotate_instead). An API key is revoked by the person who holds it, whatever it reaches, or by a person whose own access covers the key's access (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. Revoking an API key also revokes the keys it replaced in its rotation that are still in their overlap and that you may revoke (predecessors_revoked_count; predecessors_left counts the rest), so its webhooks stop at once; the keys that replaced it carry its work on. Revoking a key already revoked does the same for those keys. What the credential set up or owns is listed in standing_objects for review, and for an API key also what it carries on for the keys it was rotated from, each saying whether anything still acts for it (still_acting, with the key in acting_key_id) and, for a webhook, whether it is still delivering. An organization-owned object (org_owned true) keeps working; any other keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to, except that a block filter an app made keeps blocking, within the reach the app had, unless the person revoking chose to stop it. An API key (kind org_key or connected_key) that is already revoked, and whose record is kept, answers 200 with already_revoked true, to a caller who could have revoked it, and revoked counts only the keys it replaced that were revoked now; anyone else gets the answer they would get for a key that does not exist. The person's own Primitive CLI login revokes an API key (kind org_key) as the person, with no step-up unless the organization's credential policy sets cli_writes_require_approval (then it takes a keys.create step-up); every other kind (a connected key named as org_key included, 403 grant_forbidden target_not_covered), and every other app even with a step-up, is refused outside the dashboard (403). Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session or the person's own Primitive CLI token; no other OAuth app. The person's role must hold keys:manage. API keys, agent connection keys and agents are refused.",
10561
10561
  "hasJsonBody": true,
10562
10562
  "method": "POST",
10563
10563
  "operationId": "revokeCredential",
@@ -10670,7 +10670,7 @@ const operationManifest = [
10670
10670
  }
10671
10671
  },
10672
10672
  "already_revoked": {
10673
- "description": "true when the API key was already revoked, so nothing changed and revoked is 0. Absent otherwise. GET /v1/keys/{id} says when and by whom, to a caller who may read the key.",
10673
+ "description": "true when the API key was already revoked. revoked then counts only the keys it replaced that were revoked now. Absent otherwise. GET /v1/keys/{id} says when and by whom, to a caller who may read the key.",
10674
10674
  "type": "boolean"
10675
10675
  },
10676
10676
  "keep_failed": {
@@ -10703,6 +10703,18 @@ const operationManifest = [
10703
10703
  "org_owned": {
10704
10704
  "type": "boolean",
10705
10705
  "description": "true: the organization owns it, and it keeps working. false: the credential owns it itself, as with everything a third-party app or agent delegate made, a webhook made with a narrowed API key, or a wake schedule made with a key that does not cover super_admin. It keeps working only while someone can still act for it, such as a key of the same rotation that still works, or whoever it was given to or kept it. Revoking a third-party app or agent delegate turns these off, except that a block filter it made keeps blocking, within the reach the app had, unless the person revoking chose to stop it."
10706
+ },
10707
+ "still_acting": {
10708
+ "description": "For an API key: whether anything still acts for it after this revoke (the organization, a key of the same rotation that still works, or whoever took it over). Null when that could not be read.",
10709
+ "type": ["boolean", "null"]
10710
+ },
10711
+ "acting_key_id": {
10712
+ "description": "For an API key: the key that still acts for it, shown to a caller who may read the organization's API keys. Null otherwise, and when no key does.",
10713
+ "type": ["string", "null"]
10714
+ },
10715
+ "delivering": {
10716
+ "description": "For an API key's webhook: whether it is still sent received mail, as GET /v1/endpoints/{id} delivery.delivering says. Null when that could not be read.",
10717
+ "type": ["boolean", "null"]
10706
10718
  }
10707
10719
  },
10708
10720
  "required": [
@@ -10713,6 +10725,28 @@ const operationManifest = [
10713
10725
  ],
10714
10726
  "additionalProperties": {}
10715
10727
  }
10728
+ },
10729
+ "predecessors_revoked_count": {
10730
+ "description": "For an API key: how many keys it replaced in its rotation, still in their overlap, were revoked with it. Keys that replaced it never are.",
10731
+ "type": "integer",
10732
+ "minimum": -9007199254740991,
10733
+ "maximum": 9007199254740991
10734
+ },
10735
+ "predecessors_left": {
10736
+ "description": "For an API key: how many such keys were left working because you may not revoke them. They work until their overlap ends.",
10737
+ "type": "integer",
10738
+ "minimum": -9007199254740991,
10739
+ "maximum": 9007199254740991
10740
+ },
10741
+ "predecessors_revoked": {
10742
+ "description": "The ids of the keys counted in predecessors_revoked_count, to a caller who may read the organization's API keys.",
10743
+ "type": "array",
10744
+ "items": { "type": "string" }
10745
+ },
10746
+ "predecessors_not_revoked": {
10747
+ "description": "The ids of the keys counted in predecessors_left, to a caller who may read the organization's API keys.",
10748
+ "type": "array",
10749
+ "items": { "type": "string" }
10716
10750
  }
10717
10751
  },
10718
10752
  "required": [
@@ -10731,7 +10765,7 @@ const operationManifest = [
10731
10765
  "binaryResponse": false,
10732
10766
  "bodyRequired": true,
10733
10767
  "command": "rotate-key",
10734
- "description": "Mint a successor with a new id and the same name, grant, expiry and conditions, and return its secret once. The old key keeps working for overlap_seconds, then stops: 24 hours when overlap_seconds is omitted, at most 7 days, never past its own expires_at, and at once with 0 (which is how to rotate a key whose secret leaked). previous_key_expires_at says when the old key stops. Rotating does not extend a key: the successor keeps the old expiry unless expires_at gives it a new one. That needs keys:manage (403 forbidden otherwise), is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and someone limited to their own keys can only keep or bring it forward (403 grant_forbidden, details.reason own_key_tighten_only); the old key then never outlives the new one. The person rotating must cover the key's access (403 grant_forbidden, details.reason target_not_covered), and a key an owner holds is rotated by an owner only (403 forbidden, details.reason owner_key_rotation), since rotating hands over the new secret. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10768
+ "description": "Mint a successor with a new id and the same name, grant, expiry and conditions, and return its secret once. The old key keeps working for overlap_seconds, then stops: 24 hours when overlap_seconds is omitted, at most 7 days, never past its own expires_at, and at once with 0 (which is how to rotate a key whose secret leaked). previous_key_expires_at says when the old key stops. Revoking the successor during the overlap also revokes the old key when you may revoke that one too; the revoke's predecessors_left counts any left working. Rotating does not extend a key: the successor keeps the old expiry unless expires_at gives it a new one. That needs keys:manage (403 forbidden otherwise), is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and someone limited to their own keys can only keep or bring it forward (403 grant_forbidden, details.reason own_key_tighten_only); the old key then never outlives the new one. The person rotating must cover the key's access (403 grant_forbidden, details.reason target_not_covered), and a key an owner holds is rotated by an owner only (403 forbidden, details.reason owner_key_rotation), since rotating hands over the new secret. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10735
10769
  "hasJsonBody": true,
10736
10770
  "method": "POST",
10737
10771
  "operationId": "rotateKey",
@@ -10905,7 +10939,7 @@ const operationManifest = [
10905
10939
  "binaryResponse": false,
10906
10940
  "bodyRequired": true,
10907
10941
  "command": "update-key",
10908
- "description": "Rename a key, change its expiry, its grant or its conditions, or suspend it. A later expires_at extends the key, which is how to keep a key working past its expiry (rotating keeps it, unless the rotation gives the new key an expires_at): it is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and like any loosening needs the person to be allowed to create a key with the key's access. An expired key's expiry cannot be changed (409 conflict, details.reason key_expired); make a new key instead. Any change needs the person's own access to cover the key's access now (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. A grant change is a new version of the key and is held to the same rules as creating one (a custom grant that does not say which mail it reaches is refused with details.reason mail_scope_required), so it takes the same step-up. It replaces the whole grant: send expected_grant_ref (grant.ref from GET /v1/keys/{id}) to have it refused (409 conflict, details.reason grant_changed) if the grant changed since that read; a grant change sent without it answers grant_unguarded: true. On a key limited to selected resources (grant.resources), a grant change must drop the limits on purpose with a custom grant and grant.resources {}; one that leaves them out is refused (409 conflict, details.reason key_has_resource_limits) and nothing changes. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10942
+ "description": "Rename a key, change its expiry, its grant or its conditions, or suspend it. A later expires_at extends the key, which is how to keep a key working past its expiry (rotating keeps it, unless the rotation gives the new key an expires_at): it is held to the credential policy (400 when past its maximum lifetime, with the latest expiry it allows now in the message), and like any loosening needs the person to be allowed to create a key with the key's access. An expired key's expiry cannot be changed (409 conflict, details.reason key_expired); make a new key instead. Any change needs the person's own access to cover the key's access now (an owner covers every key); otherwise 403 grant_forbidden, details.reason target_not_covered. The one exception is suspending a key (suspended true with no other change; expected_grant_ref may accompany it), which the person who holds it may do whatever the key reaches, calling as themself rather than through another app; lifting a suspension is a change like any other. A grant change is a new version of the key and is held to the same rules as creating one (a custom grant that does not say which mail it reaches is refused with details.reason mail_scope_required), so it takes the same step-up. It replaces the whole grant: send expected_grant_ref (grant.ref from GET /v1/keys/{id}) to have it refused (409 conflict, details.reason grant_changed) if the grant changed since that read; a grant change sent without it answers grant_unguarded: true. On a key limited to selected resources (grant.resources), a grant change must drop the limits on purpose with a custom grant and grant.resources {}; one that leaves them out is refused (409 conflict, details.reason key_has_resource_limits) and nothing changes. The person's own Primitive CLI login needs no step-up (it acts as the person, held to the same rules) unless the organization's credential policy sets cli_writes_require_approval; the Primitive app's token is refused, even with one. Needs the organization's credential features to be enabled; while they are not, this answers 404 feature_disabled, with details.feature naming the feature. Contact Primitive to turn them on.\n\nPerson only: a dashboard session, the person's own Primitive CLI login with no step-up (unless the organization's credential policy sets cli_writes_require_approval), or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one any other OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role, or the client, could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.",
10909
10943
  "hasJsonBody": true,
10910
10944
  "method": "PATCH",
10911
10945
  "operationId": "updateKey",
@@ -12917,7 +12951,7 @@ const operationManifest = [
12917
12951
  "binaryResponse": false,
12918
12952
  "bodyRequired": false,
12919
12953
  "command": "accept-email-rule-request",
12920
- "description": "Writes the rule as yours, over your own mailboxes only, turns it on, and shares what it matches from now on with the member who asked. Send no body to accept it as asked, or change it first with english and tree together (and an explanation, or the server writes one). Because this gives your mail to someone else, it is limited to the dashboard and the Primitive CLI: other OAuth apps and agent tokens are refused. Before accepting, test it on your mail: pass the request's tree to POST /v1/email-rules/test. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up. Nothing is stored, and it needs no access beyond testing a rule of your own. Accepting also needs mail:share_own (sharing your own mail through email rules, which only shows it and never sends): a role without it is refused, whether or not it can send. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
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.",
12921
12955
  "hasJsonBody": true,
12922
12956
  "method": "POST",
12923
12957
  "operationId": "acceptEmailRuleRequest",
@@ -13034,6 +13068,112 @@ const operationManifest = [
13034
13068
  },
13035
13069
  "required": ["checked", "matched"]
13036
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
+ }
13037
13177
  }
13038
13178
  },
13039
13179
  "additionalProperties": false
@@ -13188,6 +13328,14 @@ const operationManifest = [
13188
13328
  "decline_reason": {
13189
13329
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
13190
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"]
13191
13339
  }
13192
13340
  },
13193
13341
  "required": [
@@ -13234,6 +13382,10 @@ const operationManifest = [
13234
13382
  "type": "string",
13235
13383
  "enum": ["grant", "do_not_share"]
13236
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
+ },
13237
13389
  "evaluation_scope": {
13238
13390
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
13239
13391
  "type": "string",
@@ -13564,6 +13716,10 @@ const operationManifest = [
13564
13716
  "rule",
13565
13717
  "rule_id"
13566
13718
  ]
13719
+ },
13720
+ "notice": {
13721
+ "description": "What is now shared, in words.",
13722
+ "type": "string"
13567
13723
  }
13568
13724
  },
13569
13725
  "required": [
@@ -13663,7 +13819,7 @@ const operationManifest = [
13663
13819
  "binaryResponse": false,
13664
13820
  "bodyRequired": true,
13665
13821
  "command": "compile-email-rule",
13666
- "description": "Turns a plain-English description of which received emails to match into a rule tree over the published column contract, with a plain-English explanation of what it matches, the assumptions made, and warnings for anything the columns cannot express. Where the columns fall short the rule may carry up to three yes/no questions, asked about an email only after its conditions match. Nothing is saved. A rule with no questions has its conditions counted, read only, over the caller's recent classified mail (what a test of it covers by default): breadth is that count, and a warning says when it matches nearly all of it. The SQL the rule renders to is returned only when include_sql is true. kind do_not_share compiles a do-not-share rule for recall: conditions only as narrow as the description says, the judgement of what it describes put into questions, and with nothing a column decides for certain, conditions that hold for every email. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
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.",
13667
13823
  "hasJsonBody": true,
13668
13824
  "method": "POST",
13669
13825
  "operationId": "compileEmailRule",
@@ -13746,7 +13902,7 @@ const operationManifest = [
13746
13902
  "warnings": {
13747
13903
  "type": "array",
13748
13904
  "items": { "type": "string" },
13749
- "description": "Parts of the description the column contract cannot express, and how the rule handles them. When the rule reads automated or from_mailing_list, it also has a note on what that column reads and what it misses. When the rule has no questions and its conditions hold for all of the caller's recent classified mail (what a test of it covers by default), or for at least 90% of at least 20 emails, the first warning says so. That count is not made for a caller whose access shows headers only, or who could not run a test of the rule, and may also be skipped when the compile ran long or the count fails."
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."
13750
13906
  },
13751
13907
  "compiled_by": {
13752
13908
  "type": "string",
@@ -13805,7 +13961,7 @@ const operationManifest = [
13805
13961
  "binaryResponse": false,
13806
13962
  "bodyRequired": true,
13807
13963
  "command": "create-email-rule",
13808
- "description": "Saves a rule as version 1 from a compile response or a tree you wrote, checked against the column contract; without an explanation the server writes one. It starts in observe (what it matches is recorded, nothing granted or blocked) unless state is draft (not evaluated). You are its author, and it never reaches mail you cannot read. evaluation_scope is own_mailboxes (your own mail) unless you name author (all you can read) or selected; scope_selection may name only what you can read. The answer carries the scope as stored. expectations saves emails it must match and must not; the answer judges them (expectations_check). kind do_not_share blocks sharing: it runs before every other rule, and while active no rule shares an email it matches or cannot decide; a key cannot list it (details.reason rule_blocks_sharing). Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
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.",
13809
13965
  "hasJsonBody": true,
13810
13966
  "method": "POST",
13811
13967
  "operationId": "createEmailRule",
@@ -13836,9 +13992,13 @@ const operationManifest = [
13836
13992
  "enum": ["grant", "do_not_share"]
13837
13993
  },
13838
13994
  "state": {
13839
- "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.",
13840
13996
  "type": "string",
13841
- "enum": ["draft", "observe"]
13997
+ "enum": [
13998
+ "draft",
13999
+ "observe",
14000
+ "active"
14001
+ ]
13842
14002
  },
13843
14003
  "evaluation_scope": {
13844
14004
  "type": "string",
@@ -13899,6 +14059,112 @@ const operationManifest = [
13899
14059
  "additionalProperties": false,
13900
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."
13901
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
+ },
13902
14168
  "english": {
13903
14169
  "type": "string",
13904
14170
  "minLength": 1,
@@ -14015,6 +14281,10 @@ const operationManifest = [
14015
14281
  "type": "string",
14016
14282
  "enum": ["grant", "do_not_share"]
14017
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
+ },
14018
14288
  "evaluation_scope": {
14019
14289
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
14020
14290
  "type": "string",
@@ -14206,8 +14476,114 @@ const operationManifest = [
14206
14476
  ]
14207
14477
  }
14208
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
+ },
14209
14585
  "expectations_check": {
14210
- "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
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.",
14211
14587
  "type": "object",
14212
14588
  "properties": {
14213
14589
  "version": {
@@ -14219,7 +14595,7 @@ const operationManifest = [
14219
14595
  "type": ["boolean", "null"]
14220
14596
  },
14221
14597
  "decided": {
14222
- "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.",
14223
14599
  "type": "boolean"
14224
14600
  },
14225
14601
  "failed": {
@@ -14236,8 +14612,24 @@ const operationManifest = [
14236
14612
  "format": "uuid"
14237
14613
  }
14238
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
+ },
14239
14631
  "note": {
14240
- "description": "On an update that saved a version that did not pass: that it was saved anyway, is current, and whether it is live.",
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.",
14241
14633
  "type": "string"
14242
14634
  }
14243
14635
  },
@@ -14591,6 +14983,14 @@ const operationManifest = [
14591
14983
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14592
14984
  "type": ["string", "null"]
14593
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
+ },
14594
14994
  "target_can_accept": {
14595
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.",
14596
14996
  "type": ["boolean", "null"]
@@ -14811,6 +15211,14 @@ const operationManifest = [
14811
15211
  "decline_reason": {
14812
15212
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
14813
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"]
14814
15222
  }
14815
15223
  },
14816
15224
  "required": [
@@ -14968,6 +15376,320 @@ const operationManifest = [
14968
15376
  "tag": "Email Rules",
14969
15377
  "tagCommand": "email-rules"
14970
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
+ },
14971
15693
  {
14972
15694
  "binaryResponse": false,
14973
15695
  "bodyRequired": false,
@@ -15016,6 +15738,10 @@ const operationManifest = [
15016
15738
  "type": "string",
15017
15739
  "enum": ["grant", "do_not_share"]
15018
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
+ },
15019
15745
  "evaluation_scope": {
15020
15746
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
15021
15747
  "type": "string",
@@ -15197,6 +15923,112 @@ const operationManifest = [
15197
15923
  }
15198
15924
  },
15199
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
+ }
15200
16032
  }
15201
16033
  },
15202
16034
  "required": [
@@ -15220,7 +16052,7 @@ const operationManifest = [
15220
16052
  "binaryResponse": false,
15221
16053
  "bodyRequired": false,
15222
16054
  "command": "get-email-rule-request",
15223
- "description": "A rule request: the rule as proposed, who made it and to whom, and whether each of them is still a member. A request is visible to the member who made it, the member it was made to, and the organization's owners and admins. If you are the member asked, you can try the rule as proposed before answering: pass the request's tree to POST /v1/email-rules/test to test it on mail you have received. A test on mail already received shows only what has arrived. To see what the rule would do with an email that has not, such as one you would never want shared, pass the request's tree to POST /v1/email-rules/test-sample (testEmailRuleOnSample; in the Primitive CLI, primitive email-rules test-email-rule-on-sample --body-file sample.json) with an email you make up. Nothing is stored, and it needs no access beyond testing a rule of your own. Needs mail:read, a member's own credential with their full access (not an API key, agent key or narrowed token), and the email rules feature.",
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.",
15224
16056
  "hasJsonBody": false,
15225
16057
  "method": "GET",
15226
16058
  "operationId": "getEmailRuleRequest",
@@ -15384,6 +16216,14 @@ const operationManifest = [
15384
16216
  "decline_reason": {
15385
16217
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
15386
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"]
15387
16227
  }
15388
16228
  },
15389
16229
  "required": [
@@ -16266,6 +17106,14 @@ const operationManifest = [
16266
17106
  "decline_reason": {
16267
17107
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
16268
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"]
16269
17117
  }
16270
17118
  },
16271
17119
  "required": [
@@ -17409,6 +18257,10 @@ const operationManifest = [
17409
18257
  "type": "string",
17410
18258
  "enum": ["grant", "do_not_share"]
17411
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
+ },
17412
18264
  "evaluation_scope": {
17413
18265
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
17414
18266
  "type": "string",
@@ -17841,10 +18693,114 @@ const operationManifest = [
17841
18693
  "type": "string",
17842
18694
  "format": "uuid"
17843
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
+ }
17844
18800
  }
17845
18801
  },
17846
18802
  "additionalProperties": false,
17847
- "description": "Emails the rule must match and must not, at most 100 in all, always tested whatever their age: added to email_ids, or to the recent window, which gives up a place for each. The answer's expectations judge each."
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."
17848
18804
  }
17849
18805
  },
17850
18806
  "additionalProperties": false
@@ -18226,7 +19182,7 @@ const operationManifest = [
18226
19182
  }
18227
19183
  },
18228
19184
  "expectations": {
18229
- "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.",
18230
19186
  "type": "object",
18231
19187
  "properties": {
18232
19188
  "passed": {
@@ -18296,6 +19252,71 @@ const operationManifest = [
18296
19252
  ]
18297
19253
  },
18298
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
+ }
18299
19320
  }
18300
19321
  },
18301
19322
  "required": [
@@ -18855,6 +19876,217 @@ const operationManifest = [
18855
19876
  "additionalProperties": false,
18856
19877
  "description": "Replaces its saved expectations (author only); null removes them."
18857
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
+ },
18858
20090
  "if_revision": {
18859
20091
  "description": "The revision you read: if the rule was updated since, nothing is saved (409 email_rule_conflict, revision_moved).",
18860
20092
  "type": "integer",
@@ -18895,6 +20127,10 @@ const operationManifest = [
18895
20127
  "type": "string",
18896
20128
  "enum": ["grant", "do_not_share"]
18897
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
+ },
18898
20134
  "evaluation_scope": {
18899
20135
  "description": "Whose mail it runs over, as stored (GET /email-rules/{id}/scope: what that is now).",
18900
20136
  "type": "string",
@@ -19086,8 +20322,114 @@ const operationManifest = [
19086
20322
  ]
19087
20323
  }
19088
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
+ },
19089
20431
  "expectations_check": {
19090
- "description": "After a write that saved a version or changed its expectations, if it has some: them, judged on that version from stored classifications without asking its questions. Undecided: one that needs answers not yet kept for that version, is unclassified, skipped or not reached, or that its author does not read. Absent when it could not be judged (the rule changed again, its author cannot be resolved, your access shows headers only, it could not finish in time, or the check failed). Test by rule_id for the rest. With if_expectations_pass, the judgment made before the save, which passed.",
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.",
19091
20433
  "type": "object",
19092
20434
  "properties": {
19093
20435
  "version": {
@@ -19099,7 +20441,7 @@ const operationManifest = [
19099
20441
  "type": ["boolean", "null"]
19100
20442
  },
19101
20443
  "decided": {
19102
- "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.",
19103
20445
  "type": "boolean"
19104
20446
  },
19105
20447
  "failed": {
@@ -19116,8 +20458,24 @@ const operationManifest = [
19116
20458
  "format": "uuid"
19117
20459
  }
19118
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
+ },
19119
20477
  "note": {
19120
- "description": "On an update that saved a version that did not pass: that it was saved anyway, is current, and whether it is live.",
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.",
19121
20479
  "type": "string"
19122
20480
  }
19123
20481
  },
@@ -19355,6 +20713,14 @@ const operationManifest = [
19355
20713
  "decline_reason": {
19356
20714
  "description": "Why the member asked declined it; null unless they gave a reason when declining.",
19357
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"]
19358
20724
  }
19359
20725
  },
19360
20726
  "required": [
@@ -35371,7 +36737,7 @@ const operationManifest = [
35371
36737
  "binaryResponse": false,
35372
36738
  "bodyRequired": true,
35373
36739
  "command": "decide-suspension",
35374
- "description": "Keep, narrow or revoke one suspended or flagged item. Keeping it, and revoking an API key, needs the decider to cover it (revoking an uncovered key is refused with `grant_forbidden`, `target_not_covered`). Keeping an API key rotated (`rotate: true`, other than the shared Function key) also revokes its older secrets still in a rotation overlap, and needs keys:manage on both the credential and the person: without it, or if the decider lost members:manage meanwhile, it is refused with `grant_forbidden` (`keys_manage_required` or `actor_access_changed`) and nothing is rotated. A rule share paused by a role change (`email_rule_share`) takes keep, which leaves it paused, or revoke, which ends the share and needs confirm: true (the person it was shared with loses the emails already shared; without it, 409 `confirmation_required`, as for revoking a filter). Neither of the share's two people may keep it (`grant_forbidden`, `share_decision_forbidden`). Access a person gave on their own mailbox (`mailbox_access`) takes keep or revoke, and both need delegations:manage; revoke removes only what that person gave on that mailbox. Nobody the access reaches, nor the person who gave it, may keep it (`grant_forbidden`, `mailbox_access_decision_forbidden`). Person only: a dashboard session, or the person's own OAuth or CLI token with a session step-up of purpose `suspensions.decide` (the person confirms at the step_up_url a 403 step_up_required answer carries, which names this client, then send the token in the Primitive-Step-Up header; the token works only with the client it was confirmed for). Needs members:manage. API keys are refused.",
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.",
35375
36741
  "hasJsonBody": true,
35376
36742
  "method": "POST",
35377
36743
  "operationId": "decideSuspension",
@@ -47171,7 +48537,7 @@ function sanitizeForTerminal(value) {
47171
48537
  return out;
47172
48538
  }
47173
48539
  // src/oclif/answer-reports.ts
47174
- function isRecord$2(value) {
48540
+ function isRecord$3(value) {
47175
48541
  return value !== null && typeof value === "object" && !Array.isArray(value);
47176
48542
  }
47177
48543
  /** Text from an answer, made safe and one line for a report: no terminal controls. */
@@ -47180,10 +48546,10 @@ function oneLine(text) {
47180
48546
  }
47181
48547
  /** One line per warning a route write answers with (createRoute, updateRoute). */
47182
48548
  function routeWarningLines(data) {
47183
- const warnings = isRecord$2(data) ? data.warnings : void 0;
48549
+ const warnings = isRecord$3(data) ? data.warnings : void 0;
47184
48550
  if (!Array.isArray(warnings)) return [];
47185
48551
  return warnings.flatMap((warning) => {
47186
- if (!isRecord$2(warning)) return [];
48552
+ if (!isRecord$3(warning)) return [];
47187
48553
  const message = typeof warning.message === "string" ? oneLine(warning.message) : "";
47188
48554
  if (message) return [`Route warning: ${withCliCommands(message)}`];
47189
48555
  const code = typeof warning.code === "string" ? oneLine(warning.code) : "";
@@ -47211,11 +48577,11 @@ const DELIVERED_STATUSES = /* @__PURE__ */ new Set(["delivered", "header_confirm
47211
48577
  * and has no recorded answer says so.
47212
48578
  */
47213
48579
  function deliveryAttemptLines(data) {
47214
- const attempts = isRecord$2(data) ? data.attempts : void 0;
48580
+ const attempts = isRecord$3(data) ? data.attempts : void 0;
47215
48581
  if (!Array.isArray(attempts)) return [];
47216
48582
  const lines = [];
47217
48583
  for (const attempt of attempts) {
47218
- if (!isRecord$2(attempt)) continue;
48584
+ if (!isRecord$3(attempt)) continue;
47219
48585
  const text = (value) => typeof value === "string" && value ? oneLine(value) : "";
47220
48586
  const status = text(attempt.status);
47221
48587
  const parts = [
@@ -47309,7 +48675,7 @@ var CliOrgRefusedError = class extends Errors.CLIError {
47309
48675
  this.orgId = params.orgId;
47310
48676
  }
47311
48677
  };
47312
- function isRecord$1(value) {
48678
+ function isRecord$2(value) {
47313
48679
  return value !== null && typeof value === "object" && !Array.isArray(value);
47314
48680
  }
47315
48681
  function apiOrigin(apiBaseUrl) {
@@ -47336,7 +48702,7 @@ function isUuid(value) {
47336
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);
47337
48703
  }
47338
48704
  function isOrgAccessToken(value) {
47339
- return isRecord$1(value) && typeof value.access_token === "string" && value.access_token.length > 0 && value.token_type === "Bearer" && typeof value.expires_in === "number" && value.expires_in > 0 && typeof value.org_id === "string" && (value.org_name === null || typeof value.org_name === "string") && typeof value.oauth_grant_id === "string";
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";
47340
48706
  }
47341
48707
  function orgTokenFromResponse(token, now = Date.now) {
47342
48708
  return {
@@ -47383,7 +48749,7 @@ async function postTokenEndpoint(params) {
47383
48749
  throw new Errors.CLIError(`Could not reach the Primitive OAuth token endpoint: ${detail}`, { exit: 1 });
47384
48750
  }
47385
48751
  const payload = await response.json().catch(() => null);
47386
- if (response.ok && isRecord$1(payload)) return {
48752
+ if (response.ok && isRecord$2(payload)) return {
47387
48753
  ok: true,
47388
48754
  payload
47389
48755
  };
@@ -47391,7 +48757,7 @@ async function postTokenEndpoint(params) {
47391
48757
  ok: true,
47392
48758
  payload: {}
47393
48759
  };
47394
- const record = isRecord$1(payload) ? payload : {};
48760
+ const record = isRecord$2(payload) ? payload : {};
47395
48761
  return {
47396
48762
  ok: false,
47397
48763
  status: response.status,
@@ -47594,12 +48960,12 @@ async function fetchCliOrgs(params) {
47594
48960
  }
47595
48961
  const payload = await response.json().catch(() => null);
47596
48962
  if (!response.ok) {
47597
- const error = isRecord$1(payload) && isRecord$1(payload.error) ? payload.error : {};
48963
+ const error = isRecord$2(payload) && isRecord$2(payload.error) ? payload.error : {};
47598
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 });
47599
48965
  throw new Errors.CLIError(`Could not list organizations${typeof error.message === "string" ? `: ${error.message}` : ` (HTTP ${response.status}).`}`, { exit: 1 });
47600
48966
  }
47601
- const data = isRecord$1(payload) ? payload.data : null;
47602
- if (!isRecord$1(data) || !Array.isArray(data.orgs)) throw new Errors.CLIError("Primitive API returned an unexpected organization list.", { exit: 1 });
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 });
47603
48969
  return data;
47604
48970
  }
47605
48971
  /** How long a command waits for another CLI process to finish with the saved login. */
@@ -48172,6 +49538,358 @@ async function withAttachmentWait(call, options) {
48172
49538
  }
48173
49539
  return result;
48174
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
+ }
48175
49893
  // src/oclif/email-rule-authoring.ts
48176
49894
  const EMAIL_ID = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
48177
49895
  /** Whether `id` has the shape of an email id (a UUID). */
@@ -48184,9 +49902,6 @@ function usageError(message) {
48184
49902
  function isRecord(value) {
48185
49903
  return value !== null && typeof value === "object" && !Array.isArray(value);
48186
49904
  }
48187
- function errorDetail(error) {
48188
- return error instanceof Error ? error.message : String(error);
48189
- }
48190
49905
  /** `count` and the word for it: "1 expectation", "2 expectations". */
48191
49906
  function plural$1(count, one, many) {
48192
49907
  return `${count} ${count === 1 ? one : many}`;
@@ -48227,7 +49942,7 @@ function checkExpectations(value, label) {
48227
49942
  if (!isRecord(value)) throw usageError(`${label} must hold a JSON object such as {"match": ["<email-id>"], "no_match": ["<email-id>"]}.`);
48228
49943
  const expectations = {};
48229
49944
  for (const [key, list] of Object.entries(value)) {
48230
- if (key !== "match" && key !== "no_match") throw usageError(`${label} takes only "match" and "no_match"; "${key}" is not one of them.`);
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.`);
48231
49946
  if (!Array.isArray(list) || list.some((id) => typeof id !== "string")) throw usageError(`${label}: "${key}" must be a list of email ids.`);
48232
49947
  const ids = [];
48233
49948
  const seen = /* @__PURE__ */ new Set();
@@ -48243,33 +49958,19 @@ function checkExpectations(value, label) {
48243
49958
  assertDisjoint(expectations, label);
48244
49959
  return expectations;
48245
49960
  }
48246
- /** Read and parse a JSON file a flag names, failing like --body-file does. */
48247
- function readJsonFileFlag(path, flag) {
48248
- let text;
48249
- try {
48250
- text = readFileSync(path, "utf8");
48251
- } catch (error) {
48252
- throw new Errors.CLIError(`Could not read ${flag} ${path}: ${errorDetail(error)}`, { exit: 1 });
48253
- }
48254
- try {
48255
- return JSON.parse(text);
48256
- } catch (error) {
48257
- throw new Errors.CLIError(`${flag} ${path} is not valid JSON: ${errorDetail(error)}`, { exit: 1 });
48258
- }
48259
- }
48260
- /** `expect` for test-email-rule, from --expect-file or the id flags. */
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
+ */
48261
49965
  function expectBodyFields(flags) {
49966
+ const samples = expectSamples(flags);
48262
49967
  const file = flags["expect-file"];
48263
- if (typeof file === "string") {
48264
- const label = `--expect-file ${file}`;
48265
- return [{
48266
- flag: "--expect-file",
48267
- property: "expect",
48268
- value: checkExpectations(readJsonFileFlag(file, "--expect-file"), label)
48269
- }];
48270
- }
48271
49968
  const expect = {};
48272
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
+ }
48273
49974
  const match = flags["expect-match"];
48274
49975
  if (Array.isArray(match)) {
48275
49976
  expect.match = parseEmailIdList(match, "--expect-match");
@@ -48280,8 +49981,12 @@ function expectBodyFields(flags) {
48280
49981
  expect.no_match = parseEmailIdList(noMatch, "--expect-no-match");
48281
49982
  given.push("--expect-no-match");
48282
49983
  }
48283
- if (given.length === 0) return [];
48284
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 [];
48285
49990
  return [{
48286
49991
  flag: given.join(", "),
48287
49992
  property: "expect",
@@ -48418,18 +50123,72 @@ function expectationLine(result, results) {
48418
50123
  const note = result.status === "undecided" ? undecidedNote(undecidedReason(result, results)) : void 0;
48419
50124
  return ` ${result.status}: ${result.email_id} expected ${result.expected}, got ${result.outcome}${note ? ` (${note})` : ""}`;
48420
50125
  }
48421
- 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()) {
48422
50177
  if (!expectations) return [];
48423
50178
  const results = Array.isArray(expectations.results) ? expectations.results : [];
48424
50179
  const passed = expectations.counts?.passed ?? 0;
48425
50180
  const failed = expectations.counts?.failed ?? 0;
48426
50181
  const undecided = expectations.counts?.undecided ?? 0;
48427
50182
  if (expectations.passed && failed === 0 && undecided === 0) return [`Expectations: all ${passed} passed.`];
48428
- return [`Expectations: ${[
48429
- failed > 0 ? `${failed} failed` : "",
48430
- undecided > 0 ? `${undecided} undecided` : "",
48431
- `${passed} passed`
48432
- ].filter((part) => part !== "").join(", ")}.`, ...results.filter((result) => result.status !== "passed").map((result) => expectationLine(result, testResults))];
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
+ ];
48433
50192
  }
48434
50193
  const NOT_MATCHED_ORDER = {
48435
50194
  near_miss: 1,
@@ -48491,14 +50250,24 @@ function notMatchedLines(results, expectations) {
48491
50250
  * test can decide it;
48492
50251
  * - else nothing: the exit code stays 0.
48493
50252
  */
48494
- function expectationsOutcome(expectations, testResults = []) {
50253
+ function expectationsOutcome(expectations, testResults = [], name = sampleNamer()) {
48495
50254
  if (!expectations) return {};
48496
50255
  const results = Array.isArray(expectations.results) ? expectations.results : [];
48497
- const failed = results.filter((result) => result.status === "failed").map((result) => result.email_id);
48498
- const undecided = results.filter((result) => result.status === "undecided").map((result) => ({
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) => ({
48499
50262
  id: result.email_id,
48500
50263
  reason: undecidedReason(result, testResults)
48501
- }));
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
+ }))];
48502
50271
  const unresolvable = undecided.filter(({ reason }) => !reason.resolvable);
48503
50272
  const failedCount = Math.max(expectations.counts?.failed ?? 0, failed.length);
48504
50273
  const undecidedCount = Math.max(expectations.counts?.undecided ?? 0, undecided.length);
@@ -48514,7 +50283,7 @@ function expectationsOutcome(expectations, testResults = []) {
48514
50283
  exitCode: 1,
48515
50284
  error: {
48516
50285
  code: "expectations_unresolvable",
48517
- message: `${plural$1(unresolvable.length, "expectation is", "expectations are")} undecided, and testing again will not decide ${unresolvable.length === 1 ? "it" : "them"}: ${why(unresolvable)}.`
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)}.`
48518
50287
  }
48519
50288
  };
48520
50289
  if (undecidedCount > 0) return {
@@ -48527,8 +50296,9 @@ function expectationsOutcome(expectations, testResults = []) {
48527
50296
  return {};
48528
50297
  }
48529
50298
  /** The stderr report and exit code of a test-email-rule answer. */
48530
- function testEmailRuleReport(data) {
50299
+ function testEmailRuleReport(data, request) {
48531
50300
  if (!isRecord(data)) return { lines: [] };
50301
+ const name = sampleNamer(request);
48532
50302
  const result = data;
48533
50303
  const results = Array.isArray(result.results) ? result.results : [];
48534
50304
  const summary = isRecord(result.summary) ? result.summary : void 0;
@@ -48538,23 +50308,56 @@ function testEmailRuleReport(data) {
48538
50308
  ...scopeWarningLines(result.scope_warnings),
48539
50309
  ...conditionLines(summary),
48540
50310
  ...questionLines(summary, results),
48541
- ...expectationsLines(result.expectations, results),
50311
+ ...expectationsLines(result.expectations, results, name),
48542
50312
  ...notMatchedLines(results, result.expectations)
48543
50313
  ],
48544
- ...expectationsOutcome(result.expectations, results)
50314
+ ...expectationsOutcome(result.expectations, results, name)
48545
50315
  };
48546
50316
  }
48547
- function expectationsCheckLine(check, ruleId) {
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]);
50344
+ };
50345
+ }
50346
+ function expectationsCheckLine(check, ruleId, name = (index) => `saved sample ${index + 1}`) {
48548
50347
  if (!isRecord(check)) return void 0;
48549
50348
  const on = typeof check.version === "number" ? `version ${check.version}` : "the current version";
48550
50349
  const failed = Array.isArray(check.failed) ? check.failed : [];
48551
50350
  const undecided = Array.isArray(check.undecided) ? check.undecided : [];
48552
- 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.`;
48553
50354
  const parts = [];
48554
50355
  if (failed.length > 0) parts.push(`${failed.length} failed (${failed.join(", ")})`);
48555
- 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");
48556
50358
  if (undecided.length > 0) parts.push(`${undecided.length} undecided (${undecided.join(", ")})`);
48557
- const rest = undecided.length > 0 ? ` To decide the rest, run \`primitive email-rules test-email-rule --rule-id ${ruleId}\`.` : "";
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}\`.` : "";
48558
50361
  const note = typeof check.note === "string" && check.note.trim() ? ` ${check.note.trim()}` : "";
48559
50362
  return `Expectations on ${on}: ${parts.length > 0 ? parts.join(", ") : "not passed"}.${rest}${note}`;
48560
50363
  }
@@ -48568,7 +50371,7 @@ const FAIL_ON_EXPECTATIONS_FLAG = "fail-on-expectations";
48568
50371
  * `passed` as false or as null when nothing failed and something is
48569
50372
  * undecided; any other check that did not pass exits 1.
48570
50373
  */
48571
- function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "") {
50374
+ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "", name = (index) => `saved sample ${index + 1}`) {
48572
50375
  const test = `\`primitive email-rules test-email-rule --rule-id ${ruleId}\``;
48573
50376
  if (!isRecord(check)) {
48574
50377
  if (!sentExpectations) return {};
@@ -48580,8 +50383,9 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "")
48580
50383
  }
48581
50384
  };
48582
50385
  }
48583
- const failed = Array.isArray(check.failed) ? check.failed : [];
48584
- 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)];
48585
50389
  if (failed.length > 0) return {
48586
50390
  exitCode: 1,
48587
50391
  error: {
@@ -48593,7 +50397,7 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "")
48593
50397
  exitCode: 3,
48594
50398
  error: {
48595
50399
  code: "expectations_undecided",
48596
- message: `${saved}${plural$1(undecided.length, "expectation is", "expectations are")} undecided on this write, which judges them without classifying mail or asking the rule's questions: ${undecided.join(", ")}. Test them with ${test}.`
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}.`
48597
50401
  }
48598
50402
  };
48599
50403
  if (check.passed !== true) return {
@@ -48611,6 +50415,14 @@ function writtenExpectationsOutcome(check, sentExpectations, ruleId, saved = "")
48611
50415
  * rule active, and refuses with these same codes, saving nothing.
48612
50416
  */
48613
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
+ }
48614
50426
  /** What a restore_version write saved, as its answer says. */
48615
50427
  function restoredLine(restored, currentVersion) {
48616
50428
  if (!isRecord(restored) || typeof restored.version !== "number") return void 0;
@@ -48647,21 +50459,31 @@ function writtenEmailRuleReport(data, request) {
48647
50459
  const rule = data;
48648
50460
  const ruleId = typeof rule.id === "string" ? rule.id : "<rule-id>";
48649
50461
  const lines = scopeWarningLines(rule.scope_warnings);
48650
- if (rule.kind === "do_not_share") lines.unshift(`This rule blocks sharing (kind do_not_share): while it is active, no rule shares an email it matches.${typeof rule.state === "string" && rule.state !== "active" ? " It blocks nothing until it is active." : ""}`);
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
+ }
48651
50466
  const restored = restoredLine(rule.restored_from, rule.current_version);
48652
50467
  if (restored) lines.push(restored);
48653
50468
  const reach = affectsLine(rule.affects);
48654
50469
  if (reach) lines.push(reach);
48655
- const sentExpectations = isRecord(request.body) && isRecord(request.body.expectations);
48656
- const checked = expectationsCheckLine(rule.expectations_check, ruleId);
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);
48657
50473
  if (checked) lines.push(checked);
48658
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}\`.`);
48659
50475
  if (request.flags?.["fail-on-expectations"] !== true) return { lines };
48660
50476
  return {
48661
50477
  lines,
48662
- ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId, isUpdate(request) ? SAVED_UNJUDGED : "")
50478
+ ...writtenExpectationsOutcome(rule.expectations_check, sentExpectations, ruleId, isUpdate(request) ? SAVED_UNJUDGED : "", name)
48663
50479
  };
48664
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
+ }
48665
50487
  /** Whether a rule write is an update: it names the rule in its path. */
48666
50488
  function isUpdate(request) {
48667
50489
  return typeof request.path?.id === "string";
@@ -48673,7 +50495,7 @@ function isUpdate(request) {
48673
50495
  */
48674
50496
  function writtenFallbackExitCode(data, request) {
48675
50497
  if (request.flags["fail-on-expectations"] !== true) return void 0;
48676
- return writtenExpectationsOutcome(isRecord(data) ? data.expectations_check : void 0, isRecord(request.body) && isRecord(request.body.expectations), "<rule-id>").exitCode;
50498
+ return writtenExpectationsOutcome(isRecord(data) ? data.expectations_check : void 0, sentExpectationsIn(request.body), "<rule-id>").exitCode;
48677
50499
  }
48678
50500
  /** The stderr report of get-email-rule-scope. */
48679
50501
  function emailRuleScopeReport(data) {
@@ -48743,7 +50565,7 @@ function readAgain(id) {
48743
50565
  * expectations_undecided and no_expectations. Nothing was saved in any of
48744
50566
  * them.
48745
50567
  */
48746
- function updateEmailRuleErrorHint(code, payload, ruleId, body) {
50568
+ function updateEmailRuleErrorHint(code, payload, ruleId, body, flags) {
48747
50569
  const id = typeof ruleId === "string" && ruleId ? ruleId : "<rule-id>";
48748
50570
  const details = errorDetails(payload);
48749
50571
  const guarded = isRecord(body) && body.if_expectations_pass === true;
@@ -48761,10 +50583,43 @@ function updateEmailRuleErrorHint(code, payload, ruleId, body) {
48761
50583
  const check = isRecord(details.expectations_check) ? details.expectations_check : {};
48762
50584
  const failed = Array.isArray(check.failed) ? check.failed : [];
48763
50585
  const undecided = Array.isArray(check.undecided) ? check.undecided : [];
48764
- const parts = [...failed.length > 0 ? [`failed: ${failed.join(", ")}`] : [], ...undecided.length > 0 ? [`undecided: ${undecided.join(", ")}`] : []];
48765
- return `Hint: the rule, as this write would leave it, was judged against its expectations before saving (if_expectations_pass), and nothing was saved.${parts.length > 0 ? ` Expectations ${parts.join("; ")}.` : ""}${code === "expectations_failed" ? "" : ` ${UNDECIDED_NEXT_STEP[String(details.reason)] ?? UNDECIDED_NEXT_STEP.none}`} The command exits ${updateEmailRuleErrorExitCode(payload) ?? 1}.`;
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}.`;
48766
50597
  }
48767
- if (code === "no_expectations") return "Hint: nothing was saved. Give the rule expectations with --expectations-file (in this write or before it), or send the write without if_expectations_pass (--fail-on-expectations or --if-expectations-pass).";
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)`}.`;
48768
50623
  }
48769
50624
  /** What to do about an undecided judgment, by details.reason ("none": no reason given). */
48770
50625
  const UNDECIDED_NEXT_STEP = {
@@ -48776,21 +50631,33 @@ const UNDECIDED_NEXT_STEP = {
48776
50631
  };
48777
50632
  /**
48778
50633
  * update-email-rule's exit code for the expectations guard's refusals: 1 when
48779
- * one failed, or when they cannot be decided by trying again (the rule cannot
48780
- * run as its author); 3 when trying again later can decide them.
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.
48781
50637
  */
48782
50638
  function updateEmailRuleErrorExitCode(payload) {
48783
50639
  const code = isRecord(payload) ? payload.code : void 0;
48784
50640
  if (code === "expectations_failed") return 1;
48785
50641
  if (code !== "expectations_undecided") return void 0;
48786
- return errorDetails(payload).reason === "author_unresolvable" ? 1 : 3;
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";
48787
50653
  }
48788
50654
  /** The stderr report of compile-email-rule: how many recent emails it holds for. */
48789
50655
  function compileEmailRuleReport(data) {
48790
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()}`);
48791
50658
  const breadth = data.breadth;
48792
- if (!isRecord(breadth) || typeof breadth.checked !== "number" || typeof breadth.matched !== "number") return { lines: [] };
48793
- return { lines: [`Breadth: its conditions hold for ${breadth.matched} of ${plural$1(breadth.checked, "recent email", "recent emails")} checked in your own mailboxes, whatever scope you save it with.`] };
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 };
48794
50661
  }
48795
50662
  /** The stderr report of get-email-rule: who wrote it, and its revision. */
48796
50663
  function getEmailRuleReport(data) {
@@ -48798,13 +50665,31 @@ function getEmailRuleReport(data) {
48798
50665
  const lines = [];
48799
50666
  const author = authorLine(data.author);
48800
50667
  if (author) lines.push(author);
50668
+ const protection = protectionLine(data);
50669
+ if (protection) lines.push(protection);
48801
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.`);
48802
50671
  return { lines };
48803
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
+ }
48804
50687
  /** create-email-rule's --fail-on-expectations: the exit code follows the check made after saving. */
48805
- const FAIL_ON_EXPECTATIONS_HELP = "Exit 1 when an expectation in the answer's expectations_check failed, and 3 when none failed but one is undecided or the expectations this write sent were not checked (test the rule to decide). Without it the command exits 0 whatever the check says.";
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.";
48806
50689
  /** update-email-rule's --fail-on-expectations: the server refuses the write. */
48807
- const UPDATE_FAIL_ON_EXPECTATIONS_HELP = "Refuse the write unless the rule, as this write would leave it, passes its expectations. It sends if_expectations_pass (over any in --body-file or --raw-body), so the server judges a new version, a restored one, a new scope or turning the rule active before saving, and saves nothing when one failed (exit 1), one is undecided (exit 3, or 1 when the rule cannot run as its author) or the rule has none (exit 1). It can take as long as a test of the rule; the command waits for it. Judging needs a credential that reads the mail's content. A write the server does not judge (one changing only expectations, say) is saved, and the exit code then follows the answer's expectations_check, as on create. With --json, a refused write's document holds the API's error alone; a saved write's holds the rule.";
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.";
48808
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.";
48809
50694
  /**
48810
50695
  * update-email-rule's --fail-on-expectations sends if_expectations_pass, over
@@ -48833,9 +50718,11 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
48833
50718
  exclusive: ["expect-file"]
48834
50719
  }),
48835
50720
  "expect-file": Flags.string({
48836
- description: "A JSON file holding the request's expect, such as {\"match\": [\"<email-id>\"], \"no_match\": [\"<email-id>\"]}. Not with --expect-match or --expect-no-match.",
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.",
48837
50722
  exclusive: ["expect-match", "expect-no-match"]
48838
- })
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]) })
48839
50726
  },
48840
50727
  bodyFields: expectBodyFields,
48841
50728
  bodyProperties: ["expect"],
@@ -48848,24 +50735,55 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
48848
50735
  createEmailRule: {
48849
50736
  flags: {
48850
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]) }),
48851
50740
  [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: FAIL_ON_EXPECTATIONS_HELP })
48852
50741
  },
48853
- bodyFields: expectationsBodyFields(false),
48854
- bodyProperties: ["expectations"],
50742
+ bodyFields: (flags) => [...expectationsBodyFields(false)(flags), ...samplesBodyFields(false)(flags)],
50743
+ bodyProperties: ["expectations", "expectation_samples"],
48855
50744
  fallbackExitCode: writtenFallbackExitCode,
48856
50745
  report: writtenEmailRuleReport
48857
50746
  },
48858
50747
  updateEmailRule: {
48859
50748
  flags: {
48860
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." }),
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]) }),
48861
50759
  [FAIL_ON_EXPECTATIONS_FLAG]: Flags.boolean({ description: UPDATE_FAIL_ON_EXPECTATIONS_HELP })
48862
50760
  },
48863
- bodyFields: (flags) => [...expectationsBodyFields(true)(flags), ...expectationsGuardBodyFields(flags)],
48864
- 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
+ ],
48865
50771
  errorExitCode: updateEmailRuleErrorExitCode,
48866
50772
  fallbackExitCode: writtenFallbackExitCode,
48867
50773
  report: writtenEmailRuleReport
48868
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
+ },
48869
50787
  compileEmailRule: {
48870
50788
  flags: {},
48871
50789
  report: compileEmailRuleReport
@@ -48874,6 +50792,10 @@ const EMAIL_RULE_OPERATION_EXTENSIONS = {
48874
50792
  flags: {},
48875
50793
  report: getEmailRuleReport
48876
50794
  },
50795
+ listEmailRules: {
50796
+ flags: {},
50797
+ report: listEmailRulesReport
50798
+ },
48877
50799
  getEmailRuleScope: {
48878
50800
  flags: {},
48879
50801
  report: emailRuleScopeReport
@@ -51004,7 +52926,7 @@ function savedConnectedProfiles(configDir, exclude) {
51004
52926
  */
51005
52927
  async function otherProfiles(configDir, session, current) {
51006
52928
  if (session) try {
51007
- const { connectedProfilesForSession } = await import("./machine-session-YQWxPePp.js").then((n) => n.s);
52929
+ const { connectedProfilesForSession } = await import("./machine-session-DNnNhhFr.js").then((n) => n.s);
51008
52930
  return {
51009
52931
  inSession: true,
51010
52932
  profiles: connectedProfilesForSession(configDir, session, current)
@@ -52111,7 +54033,7 @@ const OPERATION_ERROR_HINTS = {
52111
54033
  inviteOrganizationMember: (code, body, payload) => inviteRefusalHint(code, body, payload),
52112
54034
  createEndpoint: (code, body, payload) => createEndpointRuleRequestHint(code, body, payload),
52113
54035
  updateEndpoint: (code, body, payload) => updateEndpointRuleRequestHint(code, body, payload),
52114
- updateEmailRule: (code, body, payload, path) => updateEmailRuleErrorHint(code, payload, path?.id, body),
54036
+ updateEmailRule: (code, body, payload, path, flags) => updateEmailRuleErrorHint(code, payload, path?.id, body, flags),
52115
54037
  verifyDomain: (code, _body, payload) => code === "dns_verification_failed" ? domainVerificationFailedHint(payload) : void 0,
52116
54038
  addDomain: (code, body) => {
52117
54039
  if (code === "mail_relay_unavailable") return MAIL_RELAY_UNAVAILABLE_MESSAGE;
@@ -52314,7 +54236,7 @@ function createOperationCommand(operation) {
52314
54236
  }) : await callOperation();
52315
54237
  if (result.error) {
52316
54238
  const errorPayload = extractErrorPayload(result.error);
52317
- 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);
52318
54240
  writeErrorWithHints(errorPayload, result.response, { codeHint: !(operationErrorHint && OPERATION_ERROR_HINTS_REPLACING_CODE_HINT.has(operation.sdkName)) });
52319
54241
  if (operationErrorHint) process.stderr.write(`${operationErrorHint}\n`);
52320
54242
  if (stepUpUrl) process.stderr.write(approveThenRerunHint(stepUpUrl));