@company-semantics/contracts 92.0.0 → 93.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/action-items/README.md +12 -4
- package/src/action-items/schemas.ts +10 -0
- package/src/affordances/README.md +52 -1
- package/src/affordances/__tests__/README.md +10 -1
- package/src/affordances/__tests__/affordances.test.ts +13 -1
- package/src/affordances/__tests__/catalog-wave.test.ts +237 -0
- package/src/affordances/affordance.ts +56 -4
- package/src/affordances/args.ts +44 -1
- package/src/affordances/available.ts +59 -0
- package/src/affordances/coverage.ts +51 -0
- package/src/affordances/ids.ts +28 -0
- package/src/affordances/index.ts +17 -0
- package/src/api/README.md +1 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +397 -1
- package/src/api/primitives.ts +13 -0
- package/src/billing/README.md +1 -0
- package/src/billing/refusals.ts +12 -0
- package/src/message-parts/README.md +5 -0
- package/src/message-parts/affordance-offer.ts +44 -0
- package/src/message-parts/index.ts +7 -0
- package/src/message-parts/types.ts +7 -1
- package/src/message-parts/wire.ts +18 -0
- package/src/notifications/README.md +17 -3
- package/src/notifications/__tests__/README.md +4 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +27 -25
- package/src/notifications/__tests__/fixtures.ts +7 -1
- package/src/notifications/__tests__/grounded-ctas.test.ts +103 -0
- package/src/notifications/__tests__/render-snapshot.test.ts +1 -1
- package/src/notifications/content.ts +9 -0
- package/src/notifications/kinds/billing-auto-recharge-failed.ts +4 -2
- package/src/notifications/kinds/billing-payment-past-due.ts +2 -0
- package/src/notifications/kinds/billing-suspended.ts +2 -0
- package/src/notifications/kinds/chat-shared.ts +10 -3
- package/src/notifications/kinds/company-md-access-approved.ts +13 -6
- package/src/notifications/kinds/share-granted.ts +1 -1
- package/src/notifications/kinds/unit-owner-granted.ts +9 -2
- package/src/notifications/payloads.ts +6 -0
- package/src/proactive/README.md +18 -2
- package/src/proactive/__tests__/README.md +11 -1
- package/src/proactive/__tests__/chat-templates.test.ts +7 -6
- package/src/proactive/__tests__/proactive-offers.test.ts +124 -0
- package/src/proactive/__tests__/vocabulary.test.ts +1 -1
- package/src/proactive/composer.ts +11 -0
- package/src/proactive/templates/README.md +11 -3
- package/src/proactive/templates/billing-auto-recharge-failed.ts +1 -0
- package/src/proactive/templates/billing-balance-exhausted.ts +2 -6
- package/src/proactive/templates/billing-payment-past-due.ts +1 -0
- package/src/proactive/templates/billing-subscription-canceled.ts +1 -5
- package/src/proactive/templates/billing-suspended.ts +1 -0
- package/src/proactive/templates/morning-brief.ts +1 -0
- package/src/proactive/templates/org-became-shared.ts +4 -3
- package/src/proactive/templates/source-sync-review-ready.ts +3 -8
- package/src/proactive/templates/workflow-needs-attention.ts +1 -0
- package/src/rate-limit/README.md +27 -1
- package/src/rate-limit/__tests__/README.md +23 -0
- package/src/rate-limit/__tests__/rate-limited-error.test.ts +53 -0
- package/src/rate-limit/index.ts +33 -0
- package/src/root-exports/agent-runtime.ts +14 -2
- package/src/root-exports/delivery.ts +15 -0
|
@@ -57,8 +57,10 @@ export const billingSuspendedDefinition: NotificationDefinition<"billing.suspend
|
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
type: "callToAction",
|
|
60
|
+
// cta-grounded: billing.open
|
|
60
61
|
label: "OPEN BILLING",
|
|
61
62
|
href: billingUrl,
|
|
63
|
+
affordance: { id: "billing.open" },
|
|
62
64
|
},
|
|
63
65
|
{
|
|
64
66
|
type: "notice",
|
|
@@ -15,7 +15,8 @@ import { NOTICE } from "../text";
|
|
|
15
15
|
export const chatSharedDefinition: NotificationDefinition<"chat.shared"> = {
|
|
16
16
|
kind: "chat.shared",
|
|
17
17
|
compose: (payload, context) => {
|
|
18
|
-
const { chatTitle, chatUrl, access, previewText, sharedByName } =
|
|
18
|
+
const { chatTitle, chatUrl, chatId, access, previewText, sharedByName } =
|
|
19
|
+
payload;
|
|
19
20
|
|
|
20
21
|
const items: ChatUnitItem[] = [
|
|
21
22
|
{ type: "message", role: "user", text: chatTitle, from: sharedByName },
|
|
@@ -29,7 +30,13 @@ export const chatSharedDefinition: NotificationDefinition<"chat.shared"> = {
|
|
|
29
30
|
]
|
|
30
31
|
: []),
|
|
31
32
|
{ type: "continuation" },
|
|
32
|
-
{
|
|
33
|
+
{
|
|
34
|
+
type: "callToAction",
|
|
35
|
+
// cta-grounded: chat.open
|
|
36
|
+
label: "OPEN CHAT",
|
|
37
|
+
href: chatUrl,
|
|
38
|
+
affordance: { id: "chat.open", args: { chatId } },
|
|
39
|
+
},
|
|
33
40
|
];
|
|
34
41
|
|
|
35
42
|
return {
|
|
@@ -45,7 +52,7 @@ export const chatSharedDefinition: NotificationDefinition<"chat.shared"> = {
|
|
|
45
52
|
type: "body",
|
|
46
53
|
text:
|
|
47
54
|
access === "collaborate"
|
|
48
|
-
? "A chat was shared with you
|
|
55
|
+
? "A chat was shared with you, and you are a participant."
|
|
49
56
|
: "A chat was shared with you to read.",
|
|
50
57
|
},
|
|
51
58
|
{ type: "chatUnit", items },
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Company.md access-approved notification (sent to the requester).
|
|
3
3
|
*
|
|
4
|
-
* The chat unit carries EITHER the approver's own words or the bare "
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
4
|
+
* The chat unit carries EITHER the approver's own words or the bare "The
|
|
5
|
+
* document is ready for you." line — never both. An approver who wrote a note
|
|
6
|
+
* has already said what the line would say, and stacking the two reads as a form letter with a
|
|
7
7
|
* comment bolted on. Same shape as `share.granted` (ADR-CONTRACTS-106); the call
|
|
8
8
|
* to action stays in place either way, since approval always resolves to a doc
|
|
9
9
|
* the requester can now open.
|
|
@@ -17,7 +17,8 @@ export const accessApprovedDefinition: NotificationDefinition<"companyMd.access_
|
|
|
17
17
|
{
|
|
18
18
|
kind: "companyMd.access_request_approved",
|
|
19
19
|
compose: (payload, context) => {
|
|
20
|
-
const { approverName, docTitle, accessLevel, docUrl, message } =
|
|
20
|
+
const { approverName, docTitle, accessLevel, docUrl, docId, message } =
|
|
21
|
+
payload;
|
|
21
22
|
|
|
22
23
|
return {
|
|
23
24
|
metadata: {
|
|
@@ -42,9 +43,15 @@ export const accessApprovedDefinition: NotificationDefinition<"companyMd.access_
|
|
|
42
43
|
: {
|
|
43
44
|
type: "message",
|
|
44
45
|
role: "assistant",
|
|
45
|
-
text: "
|
|
46
|
+
text: "The document is ready for you.",
|
|
46
47
|
},
|
|
47
|
-
{
|
|
48
|
+
{
|
|
49
|
+
type: "callToAction",
|
|
50
|
+
// cta-grounded: companyMd.openDoc
|
|
51
|
+
label: "OPEN",
|
|
52
|
+
href: docUrl,
|
|
53
|
+
affordance: { id: "companyMd.openDoc", args: { docId } },
|
|
54
|
+
},
|
|
48
55
|
],
|
|
49
56
|
},
|
|
50
57
|
{
|
|
@@ -28,6 +28,7 @@ export const unitOwnerGrantedDefinition: NotificationDefinition<"org.unit_owner_
|
|
|
28
28
|
unitName,
|
|
29
29
|
roleLabel,
|
|
30
30
|
ctaUrl,
|
|
31
|
+
unitId,
|
|
31
32
|
message,
|
|
32
33
|
expiresAt,
|
|
33
34
|
} = payload;
|
|
@@ -50,9 +51,15 @@ export const unitOwnerGrantedDefinition: NotificationDefinition<"org.unit_owner_
|
|
|
50
51
|
{
|
|
51
52
|
type: "message",
|
|
52
53
|
role: "assistant",
|
|
53
|
-
text:
|
|
54
|
+
text: `${unitName} is ready for you.`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: "callToAction",
|
|
58
|
+
// cta-grounded: team.open
|
|
59
|
+
label: "MANAGE TEAM",
|
|
60
|
+
href: ctaUrl,
|
|
61
|
+
affordance: { id: "team.open", args: { unitId } },
|
|
54
62
|
},
|
|
55
|
-
{ type: "callToAction", label: "MANAGE TEAM", href: ctaUrl },
|
|
56
63
|
];
|
|
57
64
|
|
|
58
65
|
const rows: KeyValueRow[] = [
|
|
@@ -72,6 +72,8 @@ export interface NotificationPayloads {
|
|
|
72
72
|
roleLabel: "Unit owner" | "Delegate";
|
|
73
73
|
/** Full URL to view the org unit in the app */
|
|
74
74
|
ctaUrl: string;
|
|
75
|
+
/** The org unit's id — the `team.open` affordance the link performs. */
|
|
76
|
+
unitId: string;
|
|
75
77
|
/** Optional message from the granter, shown in the notification and recorded with the grant */
|
|
76
78
|
message?: string;
|
|
77
79
|
/** ISO timestamp when the grant expires. Absent = permanent (formal unit owners; delegations with no expiry). Rendered as the last key/value row ("Expires: Jun 13, 2026") when present. */
|
|
@@ -110,6 +112,8 @@ export interface NotificationPayloads {
|
|
|
110
112
|
chatTitle: string;
|
|
111
113
|
/** Full URL of the chat itself — the link that opens it in the recipient's app */
|
|
112
114
|
chatUrl: string;
|
|
115
|
+
/** The chat's id — the `chat.open` affordance the link performs. */
|
|
116
|
+
chatId: string;
|
|
113
117
|
/** What the share granted: Read only or Collaborate */
|
|
114
118
|
access: "read" | "collaborate";
|
|
115
119
|
/** First ~200 chars of chat content for preview. Generated server-side. */
|
|
@@ -158,6 +162,8 @@ export interface NotificationPayloads {
|
|
|
158
162
|
accessLevel: "editor" | "commenter" | "viewer";
|
|
159
163
|
/** Full URL to view the node */
|
|
160
164
|
docUrl: string;
|
|
165
|
+
/** The company.md node's id — the `companyMd.openDoc` affordance the link performs. */
|
|
166
|
+
docId: string;
|
|
161
167
|
/**
|
|
162
168
|
* Optional note from the approver, included in the notification only (not
|
|
163
169
|
* persisted) — the approval counterpart of the deny kind's `reason`
|
package/src/proactive/README.md
CHANGED
|
@@ -176,6 +176,21 @@ the three from another.
|
|
|
176
176
|
time. The deliberate opposite of the durable-inbox rule, and both are
|
|
177
177
|
correct: a transcript that changed under the reader would be a lie about the
|
|
178
178
|
conversation.
|
|
179
|
+
- **A pushed chat offers its next steps as affordance instances, never as
|
|
180
|
+
chips or prose** (control ADR `affordance-system`; contracts ADR
|
|
181
|
+
`affordance-catalog-wave-and-grounded-ctas`). `ProactiveChatMessage.affordances`
|
|
182
|
+
is REQUIRED on every composer — an empty list is an explicit decision — and
|
|
183
|
+
holds `OfferedAffordance` instances (ids and args, never labels or hrefs),
|
|
184
|
+
frozen with the message. The backend chat writer persists them as a
|
|
185
|
+
`data-affordance-offer` part (`../message-parts/affordance-offer.ts`) and the
|
|
186
|
+
app renders them as real controls, re-gated per viewer. A chip only sends a
|
|
187
|
+
chat prompt, so a next step is never a chip, and the prose states facts
|
|
188
|
+
rather than telling the reader where to go. A projected id (`review.open`)
|
|
189
|
+
is never offered: the five billing chats offer `billing.open`,
|
|
190
|
+
`orgBecameShared` offers `workspace.openOrgSettings`, and `morningBrief`,
|
|
191
|
+
`sourceSyncReviewReady` (opening its review is the projected `review.open`,
|
|
192
|
+
and its facts carry no request id) and
|
|
193
|
+
`workflowNeedsAttention` (its outcome carries its own offer) offer nothing.
|
|
179
194
|
|
|
180
195
|
## Public API
|
|
181
196
|
|
|
@@ -211,7 +226,7 @@ the three from another.
|
|
|
211
226
|
| `SourceSyncReviewReadyFacts` | `orgName` + `checkpoint`; no count, no document name, no source name |
|
|
212
227
|
| `ExplainedWorkflowOutcomeData` | The settlement data part with an explained, fact-free outcome |
|
|
213
228
|
| `WorkflowNeedsAttentionFacts` | `narration` (backend settlement copy) + the fact-free `outcome` data part |
|
|
214
|
-
| `ProactiveChatMessage` | `title`, `text`, 0..4
|
|
229
|
+
| `ProactiveChatMessage` | `title`, `text`, 0..4 chips, the offered `affordances`, an optional `outcome` |
|
|
215
230
|
| `ProactiveChatComposer` | The composer port — `template` plus an arrow-property `compose(facts)` |
|
|
216
231
|
| `PROACTIVE_CHAT_MAX_REPLIES` | At most this many chips (4); more is a menu, not a chip row |
|
|
217
232
|
| `PROACTIVE_CHAT_COMPOSERS` | The total template → composer registry (see `templates/README.md`) |
|
|
@@ -222,7 +237,8 @@ Type-only imports of `UserNotificationKind` (`../user-notifications`),
|
|
|
222
237
|
`NotificationKind` (`../notifications`) and `ActionItemKind`
|
|
223
238
|
(`../action-items`) — the three vocabularies this one composes — and of
|
|
224
239
|
`SuggestedReply` (`../message-parts`), the chip shape a composer's `replies`
|
|
225
|
-
reuse rather than redeclare
|
|
240
|
+
reuse rather than redeclare, and of `OfferedAffordance` (`../affordances`),
|
|
241
|
+
the instance shape a composer's `affordances` offer. Type-only imports of `WorkflowOutcomeData`
|
|
226
242
|
(`../message-parts`), `ExplainedWorkflowOutcome` (`../workflows`) and
|
|
227
243
|
`WorkflowType` for the workflow settlement chat; the dependency runs
|
|
228
244
|
proactive → workflows, never the reverse. No `zod`, no runtime imports; pure vocabulary
|
|
@@ -42,7 +42,8 @@ cannot.
|
|
|
42
42
|
every template in `PROACTIVE_CHAT_TEMPLATES` has a composer whose `template`
|
|
43
43
|
equals its key (the type proves totality; this proves no entry is an
|
|
44
44
|
accidental `undefined`), the org-became-shared prose NAMES THE RELOCATION and
|
|
45
|
-
every one of the five sections that
|
|
45
|
+
every one of the five sections that moved to the workspace (offering
|
|
46
|
+
`workspace.openOrgSettings` as the destination), the
|
|
46
47
|
morning brief PERSONALIZES THROUGH ITS FACTS (two recipients of one
|
|
47
48
|
occurrence get different prose from one composer), chips stay within
|
|
48
49
|
`PROACTIVE_CHAT_MAX_REPLIES`, and composers emit content — no markup, no
|
|
@@ -58,6 +59,15 @@ cannot.
|
|
|
58
59
|
fact-free outcome (schema-valid, `facts: null`) and no chips, frames the
|
|
59
60
|
backend narration verbatim, has a title per workflow type with no digit and
|
|
60
61
|
no next-step wording; and both inbox rows supersede per workflow type.
|
|
62
|
+
- `proactive-offers.test.ts` — pushed chats offer next steps as affordance
|
|
63
|
+
instances, never as chips or prose (control ADR `affordance-system`): every
|
|
64
|
+
composer returns an explicit `affordances` list, the billing chats offer
|
|
65
|
+
`billing.open` and carry no add-funds or resubscribe chip, org-became-shared
|
|
66
|
+
offers `workspace.openOrgSettings`, no composer offers a projected id, no
|
|
67
|
+
title, text, chip label or chip prompt carries call-to-action wording (the
|
|
68
|
+
cta-prose-ratchet regex; `workflowNeedsAttention` excepted, since it frames
|
|
69
|
+
the backend's narration verbatim), and `WireSurfaceBuilder.affordanceOffer`
|
|
70
|
+
carries the offered instances verbatim.
|
|
61
71
|
- `workflow-attention-fixtures.ts` — explained, fact-free outcomes for those
|
|
62
72
|
tests and the registry-wide fixtures. A COMPILED module, so each fixture is
|
|
63
73
|
typechecked against `ExplainedWorkflowOutcome`.
|
|
@@ -82,7 +82,7 @@ describe("proactive chat templates", () => {
|
|
|
82
82
|
orgName: "Acme",
|
|
83
83
|
joinerDisplayName: "Ada",
|
|
84
84
|
});
|
|
85
|
-
expect(msg.
|
|
85
|
+
expect(msg.affordances).toEqual([{ id: "workspace.openOrgSettings" }]);
|
|
86
86
|
expect(msg.text).toContain("relocation");
|
|
87
87
|
expect(msg.replies.length).toBeLessThanOrEqual(PROACTIVE_CHAT_MAX_REPLIES);
|
|
88
88
|
});
|
|
@@ -102,7 +102,6 @@ describe("proactive chat templates", () => {
|
|
|
102
102
|
"AI usage",
|
|
103
103
|
"Spending controls",
|
|
104
104
|
"Billing",
|
|
105
|
-
"My settings",
|
|
106
105
|
]) {
|
|
107
106
|
expect(msg.text, section).toContain(section);
|
|
108
107
|
}
|
|
@@ -131,7 +130,9 @@ describe("proactive chat templates", () => {
|
|
|
131
130
|
expect(msg.text, trigger).not.toContain("accepted your invite");
|
|
132
131
|
// The half that does not vary: the relocation is the same either way.
|
|
133
132
|
expect(msg.text, trigger).toContain("relocation, not an addition");
|
|
134
|
-
expect(msg.
|
|
133
|
+
expect(msg.affordances, trigger).toEqual([
|
|
134
|
+
{ id: "workspace.openOrgSettings" },
|
|
135
|
+
]);
|
|
135
136
|
}
|
|
136
137
|
});
|
|
137
138
|
|
|
@@ -149,9 +150,9 @@ describe("proactive chat templates", () => {
|
|
|
149
150
|
"Ada accepted your invite, so Acme moved from a personal workspace to a " +
|
|
150
151
|
"shared one.\n\n" +
|
|
151
152
|
"This is a relocation, not an addition: Integrations, Invite team " +
|
|
152
|
-
"members, AI usage, Spending controls and Billing have moved
|
|
153
|
-
"
|
|
154
|
-
"belong to the workspace rather than to you.",
|
|
153
|
+
"members, AI usage, Spending controls and Billing have moved from your " +
|
|
154
|
+
"own area to the workspace's. Nothing was removed — the five sections " +
|
|
155
|
+
"now belong to the workspace rather than to you.",
|
|
155
156
|
);
|
|
156
157
|
});
|
|
157
158
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { PROJECTED_AFFORDANCE_IDS } from "../../affordances/affordance";
|
|
4
|
+
import { WireSurfaceBuilder } from "../../message-parts/wire";
|
|
5
|
+
import type { ProactiveChatMessage } from "../composer";
|
|
6
|
+
import type { ProactiveChatFacts } from "../facts";
|
|
7
|
+
import {
|
|
8
|
+
PROACTIVE_CHAT_TEMPLATES,
|
|
9
|
+
type ProactiveChatTemplate,
|
|
10
|
+
} from "../surfaces";
|
|
11
|
+
import { PROACTIVE_CHAT_COMPOSERS } from "../templates/index";
|
|
12
|
+
import {
|
|
13
|
+
DRIVE_AUTH_NARRATION,
|
|
14
|
+
DRIVE_AUTH_OUTCOME,
|
|
15
|
+
} from "./workflow-attention-fixtures";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pushed chats offer next steps as affordance instances, never as chips or
|
|
19
|
+
* prose (control ADR affordance-system; PRD-01137).
|
|
20
|
+
*/
|
|
21
|
+
const CALL_TO_ACTION =
|
|
22
|
+
/\byou can\b|\btry\b|\bagain\b|\bsettings\b|\breconnect\b|\badd funds\b|\bclick\b|\bgo to\b|\bopen\b/i;
|
|
23
|
+
|
|
24
|
+
const FACTS: { readonly [T in ProactiveChatTemplate]: ProactiveChatFacts[T] } =
|
|
25
|
+
{
|
|
26
|
+
orgBecameShared: {
|
|
27
|
+
trigger: "member_added",
|
|
28
|
+
orgName: "Acme",
|
|
29
|
+
joinerDisplayName: "Ada",
|
|
30
|
+
},
|
|
31
|
+
morningBrief: {
|
|
32
|
+
recipientDisplayName: "Ada",
|
|
33
|
+
orgName: "Acme",
|
|
34
|
+
orgLocalDateLabel: "Tuesday 25 August",
|
|
35
|
+
pendingActionItemCount: 2,
|
|
36
|
+
unreadNotificationCount: 5,
|
|
37
|
+
},
|
|
38
|
+
billingBalanceExhausted: { orgName: "Acme", autoRechargeEnabled: false },
|
|
39
|
+
billingAutoRechargeFailed: { orgName: "Acme", paymentMethodPresent: true },
|
|
40
|
+
billingPaymentPastDue: { orgName: "Acme", paymentMethodPresent: false },
|
|
41
|
+
billingSuspended: { orgName: "Acme" },
|
|
42
|
+
billingSubscriptionCanceled: { orgName: "Acme" },
|
|
43
|
+
sourceSyncReviewReady: { orgName: "Acme", checkpoint: "synthesis" },
|
|
44
|
+
workflowNeedsAttention: {
|
|
45
|
+
narration: DRIVE_AUTH_NARRATION,
|
|
46
|
+
outcome: { workflowId: "wf-1", outcome: DRIVE_AUTH_OUTCOME },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function compose(template: ProactiveChatTemplate): ProactiveChatMessage {
|
|
51
|
+
const fn = PROACTIVE_CHAT_COMPOSERS[template].compose as (
|
|
52
|
+
facts: ProactiveChatFacts[ProactiveChatTemplate],
|
|
53
|
+
) => ProactiveChatMessage;
|
|
54
|
+
return fn(FACTS[template]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const BILLING = [
|
|
58
|
+
"billingBalanceExhausted",
|
|
59
|
+
"billingAutoRechargeFailed",
|
|
60
|
+
"billingPaymentPastDue",
|
|
61
|
+
"billingSuspended",
|
|
62
|
+
"billingSubscriptionCanceled",
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
describe("proactive chat offers", () => {
|
|
66
|
+
it("every composer returns an explicit affordance offer", () => {
|
|
67
|
+
for (const template of PROACTIVE_CHAT_TEMPLATES) {
|
|
68
|
+
expect(Array.isArray(compose(template).affordances), template).toBe(true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("billing chats offer billing.open and no add-funds chip", () => {
|
|
73
|
+
for (const template of BILLING) {
|
|
74
|
+
const msg = compose(template);
|
|
75
|
+
expect(msg.affordances, template).toEqual([{ id: "billing.open" }]);
|
|
76
|
+
expect(
|
|
77
|
+
msg.replies.map((r) => r.id),
|
|
78
|
+
template,
|
|
79
|
+
).not.toContain("add-funds");
|
|
80
|
+
expect(
|
|
81
|
+
msg.replies.map((r) => r.id),
|
|
82
|
+
template,
|
|
83
|
+
).not.toContain("resubscribe");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("org became shared offers the org settings", () => {
|
|
88
|
+
expect(compose("orgBecameShared").affordances).toEqual([
|
|
89
|
+
{ id: "workspace.openOrgSettings" },
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("no composer offers a projected affordance", () => {
|
|
94
|
+
for (const template of PROACTIVE_CHAT_TEMPLATES) {
|
|
95
|
+
for (const affordance of compose(template).affordances) {
|
|
96
|
+
expect(
|
|
97
|
+
PROJECTED_AFFORDANCE_IDS as readonly string[],
|
|
98
|
+
template,
|
|
99
|
+
).not.toContain(affordance.id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("no composer text, chip label or chip prompt carries call-to-action wording", () => {
|
|
105
|
+
for (const template of PROACTIVE_CHAT_TEMPLATES) {
|
|
106
|
+
if (template === "workflowNeedsAttention") continue; // frames the backend's narration verbatim
|
|
107
|
+
const msg = compose(template);
|
|
108
|
+
expect(msg.title, template).not.toMatch(CALL_TO_ACTION);
|
|
109
|
+
expect(msg.text, template).not.toMatch(CALL_TO_ACTION);
|
|
110
|
+
for (const reply of msg.replies) {
|
|
111
|
+
expect(reply.label, template).not.toMatch(CALL_TO_ACTION);
|
|
112
|
+
expect(reply.prompt ?? "", template).not.toMatch(CALL_TO_ACTION);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("the affordance offer part carries the offered instances verbatim", () => {
|
|
118
|
+
const affordances = compose("billingSuspended").affordances;
|
|
119
|
+
expect(WireSurfaceBuilder.affordanceOffer({ affordances })).toEqual({
|
|
120
|
+
type: "data-affordance-offer",
|
|
121
|
+
data: { affordances: [{ id: "billing.open" }] },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -490,7 +490,7 @@ describe("the kind registry", () => {
|
|
|
490
490
|
expect(reply.label, checkpoint).not.toMatch(/[0-9]/);
|
|
491
491
|
expect(reply.prompt ?? "", checkpoint).not.toMatch(/[0-9]/);
|
|
492
492
|
}
|
|
493
|
-
expect(msg.replies, checkpoint).toHaveLength(
|
|
493
|
+
expect(msg.replies, checkpoint).toHaveLength(2);
|
|
494
494
|
}
|
|
495
495
|
});
|
|
496
496
|
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* reader would be a lie about the conversation.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import type { OfferedAffordance } from "../affordances/affordance";
|
|
37
38
|
import type { SuggestedReply } from "../message-parts/suggested-replies";
|
|
38
39
|
import type { WorkflowOutcomeData } from "../message-parts/workflow-outcome";
|
|
39
40
|
import type { ProactiveChatFacts } from "./facts";
|
|
@@ -66,6 +67,16 @@ export interface ProactiveChatMessage {
|
|
|
66
67
|
readonly text: string;
|
|
67
68
|
/** 0..`PROACTIVE_CHAT_MAX_REPLIES` chips. */
|
|
68
69
|
readonly replies: readonly SuggestedReply[];
|
|
70
|
+
/**
|
|
71
|
+
* The next steps this chat OFFERS, frozen with it (control ADR
|
|
72
|
+
* affordance-system). A chip only sends a chat prompt, so a next step is
|
|
73
|
+
* never a chip: it is an affordance instance the chat writer persists as a
|
|
74
|
+
* `data-affordance-offer` part and the app renders as a control, re-gated per
|
|
75
|
+
* viewer. REQUIRED — an empty list is an explicit decision. A projected id
|
|
76
|
+
* (`review.open`) is never offered here. A template that reports a workflow
|
|
77
|
+
* outcome leaves this empty: the outcome carries its own offer.
|
|
78
|
+
*/
|
|
79
|
+
readonly affordances: readonly OfferedAffordance[];
|
|
69
80
|
/**
|
|
70
81
|
* The settled workflow a template reports, when it reports one. The chat
|
|
71
82
|
* writer persists it as a `data-workflow-outcome` part beside the prose, so
|
|
@@ -31,9 +31,17 @@ chat. That is the precedent notify/'s README states for outbound notifications
|
|
|
31
31
|
- At most `PROACTIVE_CHAT_MAX_REPLIES` chips. More is a menu, not a chip row.
|
|
32
32
|
- A file per template, named after it in kebab-case. Templates do not share a
|
|
33
33
|
module — the point is that a template is self-contained.
|
|
34
|
-
- `org-became-shared.ts` NAMES THE RELOCATION: the five sections that
|
|
35
|
-
|
|
36
|
-
(the app ADR with slug
|
|
34
|
+
- `org-became-shared.ts` NAMES THE RELOCATION: the five sections that moved
|
|
35
|
+
from the person's own area to the workspace's. Unannounced, that reads as
|
|
36
|
+
things going missing (the app ADR with slug
|
|
37
|
+
`nav-posture-opens-on-invite-send`). The destination is the
|
|
38
|
+
`workspace.openOrgSettings` affordance it offers, not prose.
|
|
39
|
+
- **Every composer returns an explicit `affordances` offer** (control ADR
|
|
40
|
+
`affordance-system`): the five billing templates `[{ id: "billing.open" }]`,
|
|
41
|
+
`org-became-shared.ts` `[{ id: "workspace.openOrgSettings" }]`, the rest
|
|
42
|
+
`[]`. A next step is never a chip (a chip only sends a prompt) and never
|
|
43
|
+
prose telling the reader where to go; a projected id is never offered.
|
|
44
|
+
`../__tests__/proactive-offers.test.ts` pins all of it.
|
|
37
45
|
- `morning-brief.ts` PERSONALIZES THROUGH THE FACTS, never the occurrence. One
|
|
38
46
|
`brief.morning` occurrence per org per org-local day is shared by every
|
|
39
47
|
recipient; two people's briefs differ only because the `MorningBriefFacts`
|
|
@@ -29,7 +29,7 @@ export const billingBalanceExhaustedComposer: ProactiveChatComposer<"billingBala
|
|
|
29
29
|
text:
|
|
30
30
|
`${facts.orgName} has run out of AI balance, so AI work is paused. ` +
|
|
31
31
|
`Nothing was lost — every chat, document and work item is exactly ` +
|
|
32
|
-
`where you left it, and the assistant
|
|
32
|
+
`where you left it, and the assistant resumes answering as soon ` +
|
|
33
33
|
`as the balance is topped up.\n\n` +
|
|
34
34
|
(facts.autoRechargeEnabled
|
|
35
35
|
? `Automatic top-up is switched on, so this usually clears itself ` +
|
|
@@ -41,11 +41,6 @@ export const billingBalanceExhaustedComposer: ProactiveChatComposer<"billingBala
|
|
|
41
41
|
`\n\nThe current balance is on the billing page, which is the only ` +
|
|
42
42
|
`place it is guaranteed to be up to date.`,
|
|
43
43
|
replies: [
|
|
44
|
-
{
|
|
45
|
-
id: "add-funds",
|
|
46
|
-
label: "How do I add funds?",
|
|
47
|
-
prompt: "How do I add AI funds to this workspace?",
|
|
48
|
-
},
|
|
49
44
|
{
|
|
50
45
|
id: "auto-top-up",
|
|
51
46
|
label: "Explain automatic top-up",
|
|
@@ -59,5 +54,6 @@ export const billingBalanceExhaustedComposer: ProactiveChatComposer<"billingBala
|
|
|
59
54
|
"What exactly stops working while AI is paused, and what keeps working?",
|
|
60
55
|
},
|
|
61
56
|
],
|
|
57
|
+
affordances: [{ id: "billing.open" }],
|
|
62
58
|
}),
|
|
63
59
|
};
|
|
@@ -47,11 +47,7 @@ export const billingSubscriptionCanceledComposer: ProactiveChatComposer<"billing
|
|
|
47
47
|
prompt:
|
|
48
48
|
"What happens to the AI funds already in my wallet after cancellation?",
|
|
49
49
|
},
|
|
50
|
-
{
|
|
51
|
-
id: "resubscribe",
|
|
52
|
-
label: "How do I resubscribe?",
|
|
53
|
-
prompt: "How do I start the seat subscription again?",
|
|
54
|
-
},
|
|
55
50
|
],
|
|
51
|
+
affordances: [{ id: "billing.open" }],
|
|
56
52
|
}),
|
|
57
53
|
};
|
|
@@ -74,9 +74,9 @@ export const orgBecameSharedComposer: ProactiveChatComposer<"orgBecameShared"> =
|
|
|
74
74
|
text:
|
|
75
75
|
`${openingFor(facts)}\n\n` +
|
|
76
76
|
`This is a relocation, not an addition: Integrations, Invite team ` +
|
|
77
|
-
`members, AI usage, Spending controls and Billing have moved
|
|
78
|
-
`
|
|
79
|
-
`belong to the workspace rather than to you.`,
|
|
77
|
+
`members, AI usage, Spending controls and Billing have moved from your ` +
|
|
78
|
+
`own area to the workspace's. Nothing was removed — the five sections ` +
|
|
79
|
+
`now belong to the workspace rather than to you.`,
|
|
80
80
|
replies: [
|
|
81
81
|
{
|
|
82
82
|
id: "plain-english",
|
|
@@ -96,5 +96,6 @@ export const orgBecameSharedComposer: ProactiveChatComposer<"orgBecameShared"> =
|
|
|
96
96
|
prompt: "Who can see what in my workspace now that it is shared?",
|
|
97
97
|
},
|
|
98
98
|
],
|
|
99
|
+
affordances: [{ id: "workspace.openOrgSettings" }],
|
|
99
100
|
}),
|
|
100
101
|
};
|
|
@@ -36,17 +36,11 @@ export const sourceSyncReviewReadyComposer: ProactiveChatComposer<"sourceSyncRev
|
|
|
36
36
|
`strategy documents and have proposed changes for you to ` +
|
|
37
37
|
`review.\n\n` +
|
|
38
38
|
`Nothing has been changed. Each document is updated only when ` +
|
|
39
|
-
`you say so, as an ordinary version
|
|
39
|
+
`you say so, as an ordinary version that can be restored, and ` +
|
|
40
40
|
`anything a person has edited is left alone unless you choose ` +
|
|
41
41
|
`otherwise.`) +
|
|
42
|
-
`\n\
|
|
43
|
-
`nothing happens.`,
|
|
42
|
+
`\n\nThe review waits for you. If you leave it, nothing happens.`,
|
|
44
43
|
replies: [
|
|
45
|
-
{
|
|
46
|
-
id: "open-review",
|
|
47
|
-
label: "Open the review",
|
|
48
|
-
prompt: "Open the source review.",
|
|
49
|
-
},
|
|
50
44
|
{
|
|
51
45
|
id: "what-was-found",
|
|
52
46
|
label: "What did you find?",
|
|
@@ -60,5 +54,6 @@ export const sourceSyncReviewReadyComposer: ProactiveChatComposer<"sourceSyncRev
|
|
|
60
54
|
"If I attach a source to a document, who will be able to see it?",
|
|
61
55
|
},
|
|
62
56
|
],
|
|
57
|
+
affordances: [],
|
|
63
58
|
}),
|
|
64
59
|
};
|
package/src/rate-limit/README.md
CHANGED
|
@@ -35,6 +35,29 @@ interface RateLimitResult {
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### RateLimitedErrorResponseSchema
|
|
39
|
+
|
|
40
|
+
The 429 body every rate-limited response carries — exactly what backend's
|
|
41
|
+
shared limiter sends, plus the reset fact:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
{
|
|
45
|
+
error: "RATE_LIMIT_EXCEEDED"; // RATE_LIMITED_ERROR_CODE
|
|
46
|
+
scope?: "email" | "ip" | "global";
|
|
47
|
+
message: string;
|
|
48
|
+
retryAfterSeconds: number | null; // whole seconds until the window resets
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The object is strict and `retryAfterSeconds` is REQUIRED: it mirrors the
|
|
53
|
+
`Retry-After` header, or is null when the limiter cannot say.
|
|
54
|
+
|
|
55
|
+
**Facts only.** `retryAfterSeconds` is a fact, so the message states it
|
|
56
|
+
("Too many requests. Available in 30s.") and never tells the reader to retry.
|
|
57
|
+
A retry control, if a surface has one, is that surface's own control label
|
|
58
|
+
(control ADR affordance-system). Distinct from `ShedResponse` in
|
|
59
|
+
`src/pressure.ts`, which is the load-shed body only.
|
|
60
|
+
|
|
38
61
|
## Semantic Convention
|
|
39
62
|
|
|
40
63
|
This domain uses `allowed: boolean` where `true` means the request is permitted.
|
|
@@ -54,9 +77,12 @@ This is the canonical semantic across all Company Semantics repos.
|
|
|
54
77
|
|
|
55
78
|
## Public API
|
|
56
79
|
|
|
80
|
+
- `RATE_LIMITED_ERROR_CODE`
|
|
57
81
|
- `RateLimitConfig` _(type)_
|
|
58
82
|
- `RateLimitResult` _(type)_
|
|
59
83
|
- `RateLimitTier`
|
|
84
|
+
- `RateLimitedErrorResponse` _(type)_
|
|
85
|
+
- `RateLimitedErrorResponseSchema`
|
|
60
86
|
|
|
61
87
|
<!-- END GENERATED: readme-public-api -->
|
|
62
88
|
|
|
@@ -70,6 +96,6 @@ _None._
|
|
|
70
96
|
|
|
71
97
|
**External packages:**
|
|
72
98
|
|
|
73
|
-
|
|
99
|
+
- `zod`
|
|
74
100
|
|
|
75
101
|
<!-- END GENERATED: readme-dependencies -->
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# rate-limit/\_\_tests\_\_/
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Locks the shape of the rate-limited error envelope.
|
|
6
|
+
|
|
7
|
+
- `rate-limited-error.test.ts` — proves a rate-limited body carries
|
|
8
|
+
`retryAfterSeconds` as whole non-negative seconds or null and is refused
|
|
9
|
+
without it, refuses any code other than `RATE_LIMIT_EXCEEDED`, and that the
|
|
10
|
+
schema and code are reachable from the package root as the same binding.
|
|
11
|
+
|
|
12
|
+
## Invariants
|
|
13
|
+
|
|
14
|
+
- These assert VOCABULARY and SHAPE, never limiter behavior. Window sizes and
|
|
15
|
+
which routes are limited belong to backend.
|
|
16
|
+
|
|
17
|
+
## Public API
|
|
18
|
+
|
|
19
|
+
None — test-only.
|
|
20
|
+
|
|
21
|
+
## Dependencies
|
|
22
|
+
|
|
23
|
+
`vitest`, `../index` and the package root.
|