@company-semantics/contracts 90.0.0 → 91.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 +2 -2
- package/src/affordances/README.md +65 -0
- package/src/affordances/__tests__/README.md +30 -0
- package/src/affordances/__tests__/affordances.test.ts +251 -0
- package/src/affordances/affordance.ts +124 -0
- package/src/affordances/args.ts +59 -0
- package/src/affordances/availability.ts +78 -0
- package/src/affordances/ids.ts +45 -0
- package/src/affordances/index.ts +39 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +59 -1
- package/src/generated/openapi-routes.ts +1 -0
- package/src/proactive/README.md +26 -4
- package/src/proactive/__tests__/README.md +10 -0
- package/src/proactive/__tests__/chat-templates.test.ts +8 -0
- package/src/proactive/__tests__/vocabulary.test.ts +11 -0
- package/src/proactive/__tests__/workflow-attention-fixtures.ts +49 -0
- package/src/proactive/__tests__/workflow-attention.test.ts +146 -0
- package/src/proactive/composer.ts +8 -0
- package/src/proactive/facts.ts +36 -0
- package/src/proactive/index.ts +2 -0
- package/src/proactive/kinds.ts +29 -0
- package/src/proactive/registry.ts +22 -0
- package/src/proactive/surfaces.ts +2 -0
- package/src/proactive/templates/README.md +9 -1
- package/src/proactive/templates/index.ts +2 -0
- package/src/proactive/templates/workflow-needs-attention.ts +35 -0
- package/src/root-exports/README.md +1 -0
- package/src/root-exports/delivery.ts +69 -1
- package/src/user-notifications/README.md +8 -0
- package/src/user-notifications/kinds.ts +13 -0
- package/src/user-notifications/lifecycle-registry.ts +10 -0
- package/src/workflows/README.md +38 -2
- package/src/workflows/__tests__/README.md +21 -2
- package/src/workflows/__tests__/affordances-of-outcome.test.ts +99 -0
- package/src/workflows/__tests__/deferred-route.test.ts +168 -0
- package/src/workflows/__tests__/outcome.test.ts +66 -0
- package/src/workflows/affordances-of-outcome.ts +65 -0
- package/src/workflows/deferred-route.ts +240 -0
- package/src/workflows/index.ts +34 -0
- package/src/workflows/outcome.ts +114 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affordance vocabulary barrel (control ADR affordance-system).
|
|
3
|
+
* Import from '@company-semantics/contracts' (root).
|
|
4
|
+
*/
|
|
5
|
+
export { AFFORDANCE_IDS, AffordanceIdSchema } from "./ids";
|
|
6
|
+
export type { AffordanceId } from "./ids";
|
|
7
|
+
export {
|
|
8
|
+
AFFORDANCE_ARGS,
|
|
9
|
+
NO_ARG_AFFORDANCE_IDS,
|
|
10
|
+
ReviewOpenArgsSchema,
|
|
11
|
+
} from "./args";
|
|
12
|
+
export type { AffordanceArgs } from "./args";
|
|
13
|
+
export {
|
|
14
|
+
AFFORDANCE_AVAILABILITY_IDS,
|
|
15
|
+
AFFORDANCE_BINDING_KINDS,
|
|
16
|
+
AFFORDANCE_UNAVAILABLE_REASONS,
|
|
17
|
+
AffordanceAvailabilityIdSchema,
|
|
18
|
+
AffordanceAvailabilitySchema,
|
|
19
|
+
} from "./availability";
|
|
20
|
+
export type {
|
|
21
|
+
AffordanceAvailability,
|
|
22
|
+
AffordanceAvailabilityId,
|
|
23
|
+
AffordanceBindingKind,
|
|
24
|
+
AffordanceUnavailableReason,
|
|
25
|
+
} from "./availability";
|
|
26
|
+
export {
|
|
27
|
+
AFFORDANCE_AVAILABILITY_OF,
|
|
28
|
+
AFFORDANCE_BINDING_KIND_OF,
|
|
29
|
+
AffordanceSchema,
|
|
30
|
+
MAX_OFFERED_AFFORDANCES,
|
|
31
|
+
OfferedAffordanceSchema,
|
|
32
|
+
PROJECTED_AFFORDANCE_IDS,
|
|
33
|
+
sameAffordances,
|
|
34
|
+
} from "./affordance";
|
|
35
|
+
export type {
|
|
36
|
+
Affordance,
|
|
37
|
+
OfferedAffordance,
|
|
38
|
+
ProjectedAffordanceId,
|
|
39
|
+
} from "./affordance";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = 'b2a6eec5dea4' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'b2a6eec5dea4f72290740a1cf16608a826ece2e3654ba64d2bc5e3794d571809' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -4271,6 +4271,23 @@ export interface paths {
|
|
|
4271
4271
|
patch?: never;
|
|
4272
4272
|
trace?: never;
|
|
4273
4273
|
};
|
|
4274
|
+
"/api/internal-admin/quality/decisions/{id}/diagnosis": {
|
|
4275
|
+
parameters: {
|
|
4276
|
+
query?: never;
|
|
4277
|
+
header?: never;
|
|
4278
|
+
path?: never;
|
|
4279
|
+
cookie?: never;
|
|
4280
|
+
};
|
|
4281
|
+
/** A decision's diagnosis: stable vs flaky failures, failure kinds by bucket, suspected component and findings */
|
|
4282
|
+
get: operations["getQualityDecisionDiagnosis"];
|
|
4283
|
+
put?: never;
|
|
4284
|
+
post?: never;
|
|
4285
|
+
delete?: never;
|
|
4286
|
+
options?: never;
|
|
4287
|
+
head?: never;
|
|
4288
|
+
patch?: never;
|
|
4289
|
+
trace?: never;
|
|
4290
|
+
};
|
|
4274
4291
|
"/api/comments": {
|
|
4275
4292
|
parameters: {
|
|
4276
4293
|
query?: never;
|
|
@@ -8122,7 +8139,7 @@ export interface components {
|
|
|
8122
8139
|
notification: {
|
|
8123
8140
|
id: string;
|
|
8124
8141
|
/** @enum {string} */
|
|
8125
|
-
kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply" | "chat.mention" | "proactive.org_became_shared" | "proactive.brief_morning" | "proactive.billing_balance_below_half" | "proactive.billing_balance_below_threshold" | "proactive.billing_auto_recharge_succeeded" | "proactive.billing_top_up_credited" | "proactive.billing_monthly_budget_80" | "proactive.billing_monthly_budget_100" | "proactive.billing_user_monthly_limit_reached" | "proactive.billing_byo_key_invalid" | "proactive.billing_provider_price_changed" | "proactive.billing_payment_recovered" | "proactive.billing_balance_exhausted" | "proactive.billing_auto_recharge_failed" | "proactive.billing_payment_past_due" | "proactive.billing_suspended" | "proactive.billing_subscription_canceled" | "proactive.source_sync_docs_updated" | "proactive.source_sync_review_ready" | "proactive.org_structure_review_ready";
|
|
8142
|
+
kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply" | "chat.mention" | "proactive.org_became_shared" | "proactive.brief_morning" | "proactive.billing_balance_below_half" | "proactive.billing_balance_below_threshold" | "proactive.billing_auto_recharge_succeeded" | "proactive.billing_top_up_credited" | "proactive.billing_monthly_budget_80" | "proactive.billing_monthly_budget_100" | "proactive.billing_user_monthly_limit_reached" | "proactive.billing_byo_key_invalid" | "proactive.billing_provider_price_changed" | "proactive.billing_payment_recovered" | "proactive.billing_balance_exhausted" | "proactive.billing_auto_recharge_failed" | "proactive.billing_payment_past_due" | "proactive.billing_suspended" | "proactive.billing_subscription_canceled" | "proactive.source_sync_docs_updated" | "proactive.source_sync_review_ready" | "proactive.org_structure_review_ready" | "proactive.workflow_needs_attention" | "proactive.workflow_finished";
|
|
8126
8143
|
target: {
|
|
8127
8144
|
/** @enum {string} */
|
|
8128
8145
|
type: "company_md" | "org_unit" | "org" | "execution";
|
|
@@ -18064,6 +18081,47 @@ export interface operations {
|
|
|
18064
18081
|
};
|
|
18065
18082
|
};
|
|
18066
18083
|
};
|
|
18084
|
+
getQualityDecisionDiagnosis: {
|
|
18085
|
+
parameters: {
|
|
18086
|
+
query?: never;
|
|
18087
|
+
header?: never;
|
|
18088
|
+
path: {
|
|
18089
|
+
id: string;
|
|
18090
|
+
};
|
|
18091
|
+
cookie?: never;
|
|
18092
|
+
};
|
|
18093
|
+
requestBody?: never;
|
|
18094
|
+
responses: {
|
|
18095
|
+
/** @description The decision, whether its diagnosis is stored, pending or not applicable, and the report once stored */
|
|
18096
|
+
200: {
|
|
18097
|
+
headers: {
|
|
18098
|
+
[name: string]: unknown;
|
|
18099
|
+
};
|
|
18100
|
+
content?: never;
|
|
18101
|
+
};
|
|
18102
|
+
/** @description Validation error */
|
|
18103
|
+
400: {
|
|
18104
|
+
headers: {
|
|
18105
|
+
[name: string]: unknown;
|
|
18106
|
+
};
|
|
18107
|
+
content?: never;
|
|
18108
|
+
};
|
|
18109
|
+
/** @description Not an internal admin, or missing internal.manage_quality */
|
|
18110
|
+
403: {
|
|
18111
|
+
headers: {
|
|
18112
|
+
[name: string]: unknown;
|
|
18113
|
+
};
|
|
18114
|
+
content?: never;
|
|
18115
|
+
};
|
|
18116
|
+
/** @description No such decision */
|
|
18117
|
+
404: {
|
|
18118
|
+
headers: {
|
|
18119
|
+
[name: string]: unknown;
|
|
18120
|
+
};
|
|
18121
|
+
content?: never;
|
|
18122
|
+
};
|
|
18123
|
+
};
|
|
18124
|
+
};
|
|
18067
18125
|
listCommentThreads: {
|
|
18068
18126
|
parameters: {
|
|
18069
18127
|
query: {
|
|
@@ -110,6 +110,7 @@ export const openApiRoutes = {
|
|
|
110
110
|
'/api/internal-admin/quality/candidates/{id}/accept': ['POST'],
|
|
111
111
|
'/api/internal-admin/quality/candidates/{id}/reject': ['POST'],
|
|
112
112
|
'/api/internal-admin/quality/changes/manual': ['POST'],
|
|
113
|
+
'/api/internal-admin/quality/decisions/{id}/diagnosis': ['GET'],
|
|
113
114
|
'/api/internal-admin/quality/escalations': ['GET'],
|
|
114
115
|
'/api/internal-admin/quality/escalations/{id}/resolve': ['POST'],
|
|
115
116
|
'/api/internal-admin/quality/graders': ['GET'],
|
package/src/proactive/README.md
CHANGED
|
@@ -134,6 +134,23 @@ the three from another.
|
|
|
134
134
|
broken, so it gets an unread inbox row (`proactive.org_structure_review_ready`)
|
|
135
135
|
and no chat or banner. It is triggered by the committed insert of a
|
|
136
136
|
`validated` proposal, and its occurrence key is the proposal id.
|
|
137
|
+
- **The two workflow settlement kinds are one `explained` and one `notice`**
|
|
138
|
+
(control ADR-CTRL-540 phase 2; control ADR `affordance-system`). Both are
|
|
139
|
+
triggered by a ROUTED workflow settlement — the committed `deferred → routed`
|
|
140
|
+
transition of a settlement nobody was watching, routed per reason through
|
|
141
|
+
`../workflows/deferred-route` — and both key on
|
|
142
|
+
`workflow-settled:{workflowId}:{executionEpoch}`. `workflow.needs_attention`
|
|
143
|
+
is `explained`: the `workflowNeedsAttention` chat is FACT-FREE (no counts —
|
|
144
|
+
the outcome's facts are stripped), carries the settled outcome as
|
|
145
|
+
`ProactiveChatMessage.outcome` so its grounded affordances render as real
|
|
146
|
+
controls, and has NO chips (a chip only sends a chat prompt, which is not
|
|
147
|
+
grounded). Its composer frames and never narrates: `text` is the backend's
|
|
148
|
+
settlement narration verbatim, and its per-type title states no digit and
|
|
149
|
+
no next step. The inbox row is born read. `workflow.finished` is a `notice`:
|
|
150
|
+
an unread inbox row and nothing else, because telling it needs counts that
|
|
151
|
+
must re-render under the reader's authority. Their rows are
|
|
152
|
+
`proactive.workflow_needs_attention` and `proactive.workflow_finished`.
|
|
153
|
+
Neither sets `emailKind` or `actionItemKind`.
|
|
137
154
|
- **The occurrence vocabulary is closed.** `ProactiveEventId` is branded so a
|
|
138
155
|
raw string cannot stand in for an occurrence; `ProactiveRecipientReason` is a
|
|
139
156
|
closed enum because the recipients table is the frozen audit record of who
|
|
@@ -172,9 +189,9 @@ the three from another.
|
|
|
172
189
|
| `bornRead` | Whether a class's `chat` or `inbox` row is born read |
|
|
173
190
|
| `ORG_SYSTEM_EVENT_TYPES` | Mirror of the backend `org_system_event_type` pgEnum — the banner ids |
|
|
174
191
|
| `OrgSystemEventType` | Union derived from the array |
|
|
175
|
-
| `PROACTIVE_CHAT_TEMPLATES` | Which prose composes a kind's pushed chat (
|
|
192
|
+
| `PROACTIVE_CHAT_TEMPLATES` | Which prose composes a kind's pushed chat (9, incl. `workflowNeedsAttention`) |
|
|
176
193
|
| `ProactiveChatTemplate` | Union derived from the array |
|
|
177
|
-
| `PROACTIVE_EVENT_KIND_IDS` | Closed kinds (
|
|
194
|
+
| `PROACTIVE_EVENT_KIND_IDS` | Closed kinds (org/brief/chat, `billing.*`, `sourceSync.*`, `workflow.*` …) |
|
|
178
195
|
| `ProactiveEventKind` | Union derived from the array |
|
|
179
196
|
| `ProactiveEventId` | Branded id of ONE occurrence — a raw string cannot be passed as one |
|
|
180
197
|
| `PROACTIVE_RECIPIENT_REASONS` | Closed enum of WHY a recipient was addressed (the frozen audit record) |
|
|
@@ -192,7 +209,9 @@ the three from another.
|
|
|
192
209
|
| `BillingSuspendedFacts` | `orgName` — the prose needs nothing else, and may state nothing else |
|
|
193
210
|
| `BillingSubscriptionCanceledFacts` | `orgName` |
|
|
194
211
|
| `SourceSyncReviewReadyFacts` | `orgName` + `checkpoint`; no count, no document name, no source name |
|
|
195
|
-
| `
|
|
212
|
+
| `ExplainedWorkflowOutcomeData` | The settlement data part with an explained, fact-free outcome |
|
|
213
|
+
| `WorkflowNeedsAttentionFacts` | `narration` (backend settlement copy) + the fact-free `outcome` data part |
|
|
214
|
+
| `ProactiveChatMessage` | `title`, `text`, 0..4 `replies` chips, and an optional settled `outcome` |
|
|
196
215
|
| `ProactiveChatComposer` | The composer port — `template` plus an arrow-property `compose(facts)` |
|
|
197
216
|
| `PROACTIVE_CHAT_MAX_REPLIES` | At most this many chips (4); more is a menu, not a chip row |
|
|
198
217
|
| `PROACTIVE_CHAT_COMPOSERS` | The total template → composer registry (see `templates/README.md`) |
|
|
@@ -203,5 +222,8 @@ Type-only imports of `UserNotificationKind` (`../user-notifications`),
|
|
|
203
222
|
`NotificationKind` (`../notifications`) and `ActionItemKind`
|
|
204
223
|
(`../action-items`) — the three vocabularies this one composes — and of
|
|
205
224
|
`SuggestedReply` (`../message-parts`), the chip shape a composer's `replies`
|
|
206
|
-
reuse rather than redeclare.
|
|
225
|
+
reuse rather than redeclare. Type-only imports of `WorkflowOutcomeData`
|
|
226
|
+
(`../message-parts`), `ExplainedWorkflowOutcome` (`../workflows`) and
|
|
227
|
+
`WorkflowType` for the workflow settlement chat; the dependency runs
|
|
228
|
+
proactive → workflows, never the reverse. No `zod`, no runtime imports; pure vocabulary
|
|
207
229
|
data and pure helpers.
|
|
@@ -51,6 +51,16 @@ cannot.
|
|
|
51
51
|
must not be told to update one), past-due separates the seat subscription from
|
|
52
52
|
the AI balance, and suspension and cancellation each say outright that nothing
|
|
53
53
|
was deleted.
|
|
54
|
+
- `workflow-attention.test.ts` — the two workflow settlement kinds (control
|
|
55
|
+
ADR-CTRL-540 phase 2): `workflow.needs_attention` is explained (a chat and a
|
|
56
|
+
born-read inbox row, never a banner) and `workflow.finished` is a notice (an
|
|
57
|
+
unread inbox row only); the `workflowNeedsAttention` chat carries the
|
|
58
|
+
fact-free outcome (schema-valid, `facts: null`) and no chips, frames the
|
|
59
|
+
backend narration verbatim, has a title per workflow type with no digit and
|
|
60
|
+
no next-step wording; and both inbox rows supersede per workflow type.
|
|
61
|
+
- `workflow-attention-fixtures.ts` — explained, fact-free outcomes for those
|
|
62
|
+
tests and the registry-wide fixtures. A COMPILED module, so each fixture is
|
|
63
|
+
typechecked against `ExplainedWorkflowOutcome`.
|
|
54
64
|
|
|
55
65
|
## Invariants
|
|
56
66
|
|
|
@@ -6,6 +6,10 @@ import type { OrgSharedTransitionTrigger } from "../../org/shared-transition";
|
|
|
6
6
|
import { PROACTIVE_CHAT_TEMPLATES } from "../surfaces";
|
|
7
7
|
import type { ProactiveChatTemplate } from "../surfaces";
|
|
8
8
|
import { PROACTIVE_CHAT_COMPOSERS } from "../templates/index";
|
|
9
|
+
import {
|
|
10
|
+
DRIVE_AUTH_NARRATION,
|
|
11
|
+
DRIVE_AUTH_OUTCOME,
|
|
12
|
+
} from "./workflow-attention-fixtures";
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* One representative facts fixture per template. Total over
|
|
@@ -32,6 +36,10 @@ const FACTS: { readonly [T in ProactiveChatTemplate]: ProactiveChatFacts[T] } =
|
|
|
32
36
|
billingSuspended: { orgName: "Acme" },
|
|
33
37
|
billingSubscriptionCanceled: { orgName: "Acme" },
|
|
34
38
|
sourceSyncReviewReady: { orgName: "Acme", checkpoint: "candidates" },
|
|
39
|
+
workflowNeedsAttention: {
|
|
40
|
+
narration: DRIVE_AUTH_NARRATION,
|
|
41
|
+
outcome: { workflowId: "wf-1", outcome: DRIVE_AUTH_OUTCOME },
|
|
42
|
+
},
|
|
35
43
|
};
|
|
36
44
|
|
|
37
45
|
/**
|
|
@@ -37,6 +37,10 @@ import {
|
|
|
37
37
|
PLAN_FIXTURES_THE_COMPILER_REJECTS,
|
|
38
38
|
REGISTRY_FIXTURES_THE_COMPILER_REJECTS,
|
|
39
39
|
} from "./compile-fixtures";
|
|
40
|
+
import {
|
|
41
|
+
DRIVE_AUTH_NARRATION,
|
|
42
|
+
DRIVE_AUTH_OUTCOME,
|
|
43
|
+
} from "./workflow-attention-fixtures";
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* The registry, read as the union it satisfies rather than as its literal
|
|
@@ -103,6 +107,10 @@ const FRESH_FACTS: {
|
|
|
103
107
|
orgName: "Acme",
|
|
104
108
|
checkpoint: "synthesis" as const,
|
|
105
109
|
}),
|
|
110
|
+
workflowNeedsAttention: () => ({
|
|
111
|
+
narration: DRIVE_AUTH_NARRATION,
|
|
112
|
+
outcome: { workflowId: "wf-1", outcome: DRIVE_AUTH_OUTCOME },
|
|
113
|
+
}),
|
|
106
114
|
};
|
|
107
115
|
|
|
108
116
|
/**
|
|
@@ -259,6 +267,9 @@ describe("the kind registry", () => {
|
|
|
259
267
|
"sourceSync.review_ready": "proactive",
|
|
260
268
|
// The org structure row is rendered by `src/proactive/` as well.
|
|
261
269
|
"orgStructure.review_ready": "proactive",
|
|
270
|
+
// Both workflow settlement rows are rendered by `src/proactive/`.
|
|
271
|
+
"workflow.needs_attention": "proactive",
|
|
272
|
+
"workflow.finished": "proactive",
|
|
262
273
|
};
|
|
263
274
|
|
|
264
275
|
expect(Object.keys(EXPECTED_INBOX_DOMAIN).sort()).toEqual(
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explained, fact-free workflow outcomes for the `workflowNeedsAttention`
|
|
3
|
+
* template's tests. A COMPILED module (not `*.test.ts`), so every fixture is
|
|
4
|
+
* checked against `ExplainedWorkflowOutcome` — a reason that is not explained,
|
|
5
|
+
* or facts left in, fails the typecheck rather than passing silently.
|
|
6
|
+
*
|
|
7
|
+
* The narrations are fixture strings standing in for the backend's settlement
|
|
8
|
+
* copy; contracts never owns that wording.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExplainedWorkflowOutcomeData } from "../facts";
|
|
11
|
+
import type { ExplainedWorkflowOutcome } from "../../workflows/deferred-route";
|
|
12
|
+
|
|
13
|
+
export const DRIVE_AUTH_NARRATION =
|
|
14
|
+
"The search of the connected drive stopped because Google Drive no longer accepts the connection's sign-in.";
|
|
15
|
+
|
|
16
|
+
export const DRIVE_AUTH_OUTCOME: ExplainedWorkflowOutcome = {
|
|
17
|
+
workflowType: "source-sync/discover",
|
|
18
|
+
executionEpoch: 0,
|
|
19
|
+
kind: "failed",
|
|
20
|
+
reason: "drive_auth",
|
|
21
|
+
facts: null,
|
|
22
|
+
disposition: { kind: "narrate" },
|
|
23
|
+
affordances: [{ id: "integrations.connectGoogleDrive" }],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const PAUSED_BILLING_OUTCOME: ExplainedWorkflowOutcome = {
|
|
27
|
+
workflowType: "source-sync/synthesize",
|
|
28
|
+
executionEpoch: 1,
|
|
29
|
+
kind: "failed",
|
|
30
|
+
reason: "paused_billing",
|
|
31
|
+
facts: null,
|
|
32
|
+
disposition: { kind: "narrate" },
|
|
33
|
+
affordances: [{ id: "billing.open" }],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const MODEL_FAILED_OUTCOME: ExplainedWorkflowOutcome = {
|
|
37
|
+
workflowType: "org-structure/reanalyze",
|
|
38
|
+
executionEpoch: 2,
|
|
39
|
+
kind: "failed",
|
|
40
|
+
reason: "model_failed",
|
|
41
|
+
facts: null,
|
|
42
|
+
disposition: { kind: "narrate" },
|
|
43
|
+
affordances: [{ id: "orgStructure.reanalyze" }],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The settlement data part for an outcome, as the backend would hand it over. */
|
|
47
|
+
export const outcomeDataOf = (
|
|
48
|
+
outcome: ExplainedWorkflowOutcome,
|
|
49
|
+
): ExplainedWorkflowOutcomeData => ({ workflowId: "wf-1", outcome });
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two workflow settlement proactive kinds (control ADR-CTRL-540 phase 2;
|
|
3
|
+
* control ADR affordance-system): `workflow.needs_attention` is explained and
|
|
4
|
+
* its chat only FRAMES the backend's narration, `workflow.finished` is a notice.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import { CLASS_SURFACES, bornRead } from "../classes";
|
|
8
|
+
import type { ProactiveChatMessage } from "../composer";
|
|
9
|
+
import type { WorkflowNeedsAttentionFacts } from "../facts";
|
|
10
|
+
import { PROACTIVE_EVENT_KINDS } from "../registry";
|
|
11
|
+
import { PROACTIVE_CHAT_COMPOSERS } from "../templates/index";
|
|
12
|
+
import { notificationKeysOf } from "../../user-notifications/condition-keys";
|
|
13
|
+
import { NOTIFICATION_LIFECYCLE } from "../../user-notifications/lifecycle-registry";
|
|
14
|
+
import { WorkflowOutcomeSchema } from "../../workflows/outcome";
|
|
15
|
+
import { ANALYSIS_WORKFLOW_TYPES } from "../../workflows/types";
|
|
16
|
+
import type { ExplainedWorkflowOutcome } from "../../workflows/deferred-route";
|
|
17
|
+
import {
|
|
18
|
+
DRIVE_AUTH_NARRATION,
|
|
19
|
+
DRIVE_AUTH_OUTCOME,
|
|
20
|
+
MODEL_FAILED_OUTCOME,
|
|
21
|
+
PAUSED_BILLING_OUTCOME,
|
|
22
|
+
outcomeDataOf,
|
|
23
|
+
} from "./workflow-attention-fixtures";
|
|
24
|
+
|
|
25
|
+
// Mirrors the backend settlement-copy ban (backend tests/chat/workflow-outcome-copy.test.ts).
|
|
26
|
+
const CALL_TO_ACTION =
|
|
27
|
+
/\byou can\b|\btry\b|\bagain\b|\bsettings\b|\breconnect\b|\badd funds\b|\bclick\b|\bgo to\b|\bopen\b/i;
|
|
28
|
+
|
|
29
|
+
const compose = (facts: WorkflowNeedsAttentionFacts): ProactiveChatMessage =>
|
|
30
|
+
PROACTIVE_CHAT_COMPOSERS.workflowNeedsAttention.compose(facts);
|
|
31
|
+
|
|
32
|
+
const DRIVE_AUTH_FACTS: WorkflowNeedsAttentionFacts = {
|
|
33
|
+
narration: DRIVE_AUTH_NARRATION,
|
|
34
|
+
outcome: outcomeDataOf(DRIVE_AUTH_OUTCOME),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The drive-auth outcome re-typed as each workflow type, for title coverage. */
|
|
38
|
+
const outcomeAs = (
|
|
39
|
+
workflowType: (typeof ANALYSIS_WORKFLOW_TYPES)[number],
|
|
40
|
+
): ExplainedWorkflowOutcome =>
|
|
41
|
+
({ ...DRIVE_AUTH_OUTCOME, workflowType }) as ExplainedWorkflowOutcome;
|
|
42
|
+
|
|
43
|
+
describe("workflow settlement proactive kinds", () => {
|
|
44
|
+
it("workflow.needs_attention is explained: a chat and a born-read inbox row, never a banner", () => {
|
|
45
|
+
const plan = PROACTIVE_EVENT_KINDS["workflow.needs_attention"];
|
|
46
|
+
expect(plan).toEqual({
|
|
47
|
+
class: "explained",
|
|
48
|
+
chatTemplate: "workflowNeedsAttention",
|
|
49
|
+
inboxKind: "proactive.workflow_needs_attention",
|
|
50
|
+
});
|
|
51
|
+
const surfaces = CLASS_SURFACES[plan.class];
|
|
52
|
+
expect(surfaces.banner).toBe(false);
|
|
53
|
+
expect(surfaces.chat).toBe(true);
|
|
54
|
+
expect(bornRead(surfaces, "inbox")).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("workflow.finished is a notice: an unread inbox row, no banner and no chat", () => {
|
|
58
|
+
const plan = PROACTIVE_EVENT_KINDS["workflow.finished"];
|
|
59
|
+
expect(plan).toEqual({
|
|
60
|
+
class: "notice",
|
|
61
|
+
inboxKind: "proactive.workflow_finished",
|
|
62
|
+
});
|
|
63
|
+
const surfaces = CLASS_SURFACES[plan.class];
|
|
64
|
+
expect(surfaces.banner).toBe(false);
|
|
65
|
+
expect(surfaces.chat).toBe(false);
|
|
66
|
+
expect(bornRead(surfaces, "inbox")).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("the attention chat carries the fact-free outcome and no chips", () => {
|
|
70
|
+
const msg = compose(DRIVE_AUTH_FACTS);
|
|
71
|
+
expect(msg.replies).toEqual([]);
|
|
72
|
+
expect(msg.outcome).toEqual({
|
|
73
|
+
workflowId: "wf-1",
|
|
74
|
+
outcome: DRIVE_AUTH_OUTCOME,
|
|
75
|
+
});
|
|
76
|
+
expect(msg.outcome?.outcome.facts).toBeNull();
|
|
77
|
+
expect(WorkflowOutcomeSchema.safeParse(msg.outcome?.outcome).success).toBe(
|
|
78
|
+
true,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("the attention chat frames the backend narration verbatim", () => {
|
|
83
|
+
for (const outcome of [
|
|
84
|
+
DRIVE_AUTH_OUTCOME,
|
|
85
|
+
PAUSED_BILLING_OUTCOME,
|
|
86
|
+
MODEL_FAILED_OUTCOME,
|
|
87
|
+
]) {
|
|
88
|
+
const narration = `Narration for ${outcome.workflowType} ${outcome.reason}.`;
|
|
89
|
+
const msg = compose({ narration, outcome: outcomeDataOf(outcome) });
|
|
90
|
+
expect(msg.text, outcome.workflowType).toBe(narration);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("the attention chat has a title for every workflow type", () => {
|
|
95
|
+
for (const workflowType of ANALYSIS_WORKFLOW_TYPES) {
|
|
96
|
+
const msg = compose({
|
|
97
|
+
narration: DRIVE_AUTH_NARRATION,
|
|
98
|
+
outcome: outcomeDataOf(outcomeAs(workflowType)),
|
|
99
|
+
});
|
|
100
|
+
expect(typeof msg.title, workflowType).toBe("string");
|
|
101
|
+
expect(msg.title.trim().length, workflowType).toBeGreaterThan(0);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("the attention chat title states no digit and offers no next step", () => {
|
|
106
|
+
for (const workflowType of ANALYSIS_WORKFLOW_TYPES) {
|
|
107
|
+
const { title } = compose({
|
|
108
|
+
narration: DRIVE_AUTH_NARRATION,
|
|
109
|
+
outcome: outcomeDataOf(outcomeAs(workflowType)),
|
|
110
|
+
});
|
|
111
|
+
expect(title, workflowType).not.toMatch(/\d/);
|
|
112
|
+
expect(title, workflowType).not.toMatch(CALL_TO_ACTION);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("both workflow inbox rows supersede per workflow type", () => {
|
|
117
|
+
for (const kind of [
|
|
118
|
+
"proactive.workflow_needs_attention",
|
|
119
|
+
"proactive.workflow_finished",
|
|
120
|
+
] as const) {
|
|
121
|
+
const lifecycle = NOTIFICATION_LIFECYCLE[kind];
|
|
122
|
+
expect(lifecycle.condition, kind).toEqual({
|
|
123
|
+
scope: "fields",
|
|
124
|
+
fields: ["workflowType"],
|
|
125
|
+
});
|
|
126
|
+
expect(lifecycle.supersedes, kind).toBe(true);
|
|
127
|
+
expect(lifecycle.resolution, kind).toBe("never");
|
|
128
|
+
|
|
129
|
+
const first = notificationKeysOf(kind, {
|
|
130
|
+
workflowType: "source-sync/discover",
|
|
131
|
+
workflowId: "wf-1",
|
|
132
|
+
});
|
|
133
|
+
const second = notificationKeysOf(kind, {
|
|
134
|
+
workflowType: "source-sync/discover",
|
|
135
|
+
workflowId: "wf-2",
|
|
136
|
+
});
|
|
137
|
+
const other = notificationKeysOf(kind, {
|
|
138
|
+
workflowType: "org-structure/reanalyze",
|
|
139
|
+
workflowId: "wf-3",
|
|
140
|
+
});
|
|
141
|
+
expect(first.supersessionKey, kind).not.toBeNull();
|
|
142
|
+
expect(second.supersessionKey, kind).toBe(first.supersessionKey);
|
|
143
|
+
expect(other.supersessionKey, kind).not.toBe(first.supersessionKey);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import type { SuggestedReply } from "../message-parts/suggested-replies";
|
|
38
|
+
import type { WorkflowOutcomeData } from "../message-parts/workflow-outcome";
|
|
38
39
|
import type { ProactiveChatFacts } from "./facts";
|
|
39
40
|
import type { ProactiveChatTemplate } from "./surfaces";
|
|
40
41
|
|
|
@@ -65,6 +66,13 @@ export interface ProactiveChatMessage {
|
|
|
65
66
|
readonly text: string;
|
|
66
67
|
/** 0..`PROACTIVE_CHAT_MAX_REPLIES` chips. */
|
|
67
68
|
readonly replies: readonly SuggestedReply[];
|
|
69
|
+
/**
|
|
70
|
+
* The settled workflow a template reports, when it reports one. The chat
|
|
71
|
+
* writer persists it as a `data-workflow-outcome` part beside the prose, so
|
|
72
|
+
* the same renderer shows its affordances. Present only for a template whose
|
|
73
|
+
* facts carry an outcome; always fact-free here.
|
|
74
|
+
*/
|
|
75
|
+
readonly outcome?: WorkflowOutcomeData;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
// =============================================================================
|
package/src/proactive/facts.ts
CHANGED
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
* receive different prose because they were handed different facts.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import type { WorkflowOutcomeData } from "../message-parts/workflow-outcome";
|
|
26
27
|
import type { OrgSharedTransitionTrigger } from "../org/shared-transition";
|
|
28
|
+
import type { ExplainedWorkflowOutcome } from "../workflows/deferred-route";
|
|
27
29
|
|
|
28
30
|
// =============================================================================
|
|
29
31
|
// Per-template facts
|
|
@@ -189,6 +191,39 @@ export interface SourceSyncReviewReadyFacts {
|
|
|
189
191
|
readonly checkpoint: "candidates" | "synthesis";
|
|
190
192
|
}
|
|
191
193
|
|
|
194
|
+
// =============================================================================
|
|
195
|
+
// Workflow settlement facts (control ADR-CTRL-540 phase 2)
|
|
196
|
+
// =============================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The settlement data part a pushed chat may carry: an explained, fact-free
|
|
200
|
+
* outcome.
|
|
201
|
+
*/
|
|
202
|
+
export type ExplainedWorkflowOutcomeData = Omit<
|
|
203
|
+
WorkflowOutcomeData,
|
|
204
|
+
"outcome"
|
|
205
|
+
> & {
|
|
206
|
+
readonly outcome: ExplainedWorkflowOutcome;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A workflow ended in a failure a person can act on, and nobody was watching.
|
|
211
|
+
*
|
|
212
|
+
* FACT-FREE (INV-PROACTIVE-CONTENT). `narration` is the BACKEND's deterministic
|
|
213
|
+
* settlement copy (src/chat/workflow-outcome-copy.ts), rendered from the
|
|
214
|
+
* fact-stripped outcome (`facts: null`) — the same sentence the launching chat
|
|
215
|
+
* would have received, so one module owns settlement wording and it already
|
|
216
|
+
* carries the call-to-action ban. `outcome` is the settlement data part with
|
|
217
|
+
* the frozen outcome's facts stripped: reason and affordance ids only, each
|
|
218
|
+
* affordance re-gated per viewer at render and executed by the server.
|
|
219
|
+
*/
|
|
220
|
+
export interface WorkflowNeedsAttentionFacts {
|
|
221
|
+
/** The backend settlement narration for this outcome, told with `facts: null`. */
|
|
222
|
+
readonly narration: string;
|
|
223
|
+
/** `{ workflowId, outcome }` — explained route only, `facts: null`. */
|
|
224
|
+
readonly outcome: ExplainedWorkflowOutcomeData;
|
|
225
|
+
}
|
|
226
|
+
|
|
192
227
|
// =============================================================================
|
|
193
228
|
// ProactiveChatFacts
|
|
194
229
|
// =============================================================================
|
|
@@ -207,4 +242,5 @@ export interface ProactiveChatFacts {
|
|
|
207
242
|
readonly billingSuspended: BillingSuspendedFacts;
|
|
208
243
|
readonly billingSubscriptionCanceled: BillingSubscriptionCanceledFacts;
|
|
209
244
|
readonly sourceSyncReviewReady: SourceSyncReviewReadyFacts;
|
|
245
|
+
readonly workflowNeedsAttention: WorkflowNeedsAttentionFacts;
|
|
210
246
|
}
|
package/src/proactive/index.ts
CHANGED
|
@@ -51,6 +51,8 @@ export type {
|
|
|
51
51
|
BillingSuspendedFacts,
|
|
52
52
|
BillingSubscriptionCanceledFacts,
|
|
53
53
|
SourceSyncReviewReadyFacts,
|
|
54
|
+
ExplainedWorkflowOutcomeData,
|
|
55
|
+
WorkflowNeedsAttentionFacts,
|
|
54
56
|
ProactiveChatFacts,
|
|
55
57
|
} from "./facts";
|
|
56
58
|
export { PROACTIVE_CHAT_MAX_REPLIES } from "./composer";
|
package/src/proactive/kinds.ts
CHANGED
|
@@ -302,6 +302,35 @@ export const PROACTIVE_EVENT_KIND_IDS = [
|
|
|
302
302
|
* times its job is redelivered.
|
|
303
303
|
*/
|
|
304
304
|
"orgStructure.review_ready",
|
|
305
|
+
|
|
306
|
+
// ===========================================================================
|
|
307
|
+
// Workflow settlements (control ADR-CTRL-540 phase 2; control ADR
|
|
308
|
+
// affordance-system)
|
|
309
|
+
// ===========================================================================
|
|
310
|
+
//
|
|
311
|
+
// A workflow that settles with no launching conversation (a scheduled
|
|
312
|
+
// rediscovery, an upkeep re-synthesis, a launch nobody docked into a chat)
|
|
313
|
+
// is ROUTED here by the backend, per reason, through the frozen
|
|
314
|
+
// `*_DEFERRED_ROUTE_OF_REASON` tables in ../workflows/deferred-route.
|
|
315
|
+
//
|
|
316
|
+
// TRIGGER: the committed `deferred → routed` transition of the settlement
|
|
317
|
+
// row (INV-PROACTIVE-TRIGGER). OCCURRENCE KEY:
|
|
318
|
+
// `workflow-settled:{workflowId}:{executionEpoch}` — one settlement announces
|
|
319
|
+
// once, however often the sweep runs.
|
|
320
|
+
/**
|
|
321
|
+
* A workflow ended in a failure a person can act on. `explained` — a pushed
|
|
322
|
+
* chat that says what happened, FACT-FREE (reason only), carrying the frozen
|
|
323
|
+
* outcome so its grounded affordances render as controls; plus a born-read
|
|
324
|
+
* inbox row. No banner: one background run is not the workspace broken.
|
|
325
|
+
*/
|
|
326
|
+
"workflow.needs_attention",
|
|
327
|
+
/**
|
|
328
|
+
* A workflow finished (or failed with nothing to offer) while nobody was
|
|
329
|
+
* watching. A `notice` — an unread inbox row and nothing else: telling it
|
|
330
|
+
* needs counts, which are authority-sensitive and must re-render under the
|
|
331
|
+
* reader's current authority, never freeze into a chat.
|
|
332
|
+
*/
|
|
333
|
+
"workflow.finished",
|
|
305
334
|
] as const;
|
|
306
335
|
export type ProactiveEventKind = (typeof PROACTIVE_EVENT_KIND_IDS)[number];
|
|
307
336
|
|
|
@@ -262,4 +262,26 @@ export const PROACTIVE_EVENT_KINDS = {
|
|
|
262
262
|
class: "notice",
|
|
263
263
|
inboxKind: "proactive.org_structure_review_ready",
|
|
264
264
|
},
|
|
265
|
+
|
|
266
|
+
// ===========================================================================
|
|
267
|
+
// Workflow settlements (control ADR-CTRL-540 phase 2)
|
|
268
|
+
// ===========================================================================
|
|
269
|
+
//
|
|
270
|
+
// Routed per reason by the backend through ../workflows/deferred-route. NO
|
|
271
|
+
// PLAN SETS `emailKind` or `actionItemKind`: a background run's ending is
|
|
272
|
+
// announced, not standing work.
|
|
273
|
+
/**
|
|
274
|
+
* A failure a person can act on. EXPLAINED: the fact-free chat carries the
|
|
275
|
+
* badge and the frozen outcome; the inbox row is born read.
|
|
276
|
+
*/
|
|
277
|
+
"workflow.needs_attention": {
|
|
278
|
+
class: "explained",
|
|
279
|
+
chatTemplate: "workflowNeedsAttention",
|
|
280
|
+
inboxKind: "proactive.workflow_needs_attention",
|
|
281
|
+
},
|
|
282
|
+
/** A finished run nobody watched. A NOTICE: the inbox row is the attention. */
|
|
283
|
+
"workflow.finished": {
|
|
284
|
+
class: "notice",
|
|
285
|
+
inboxKind: "proactive.workflow_finished",
|
|
286
|
+
},
|
|
265
287
|
} as const satisfies Record<ProactiveEventKind, ProactiveEventDefinition>;
|
|
@@ -91,5 +91,7 @@ export const PROACTIVE_CHAT_TEMPLATES = [
|
|
|
91
91
|
"billingSubscriptionCanceled",
|
|
92
92
|
/** "A source review is ready — here is what it is and what happens if you leave it." */
|
|
93
93
|
"sourceSyncReviewReady",
|
|
94
|
+
/** "A background run did not finish — here is what happened." Fact-free. */
|
|
95
|
+
"workflowNeedsAttention",
|
|
94
96
|
] as const;
|
|
95
97
|
export type ProactiveChatTemplate = (typeof PROACTIVE_CHAT_TEMPLATES)[number];
|
|
@@ -66,6 +66,13 @@ chat. That is the precedent notify/'s README states for outbound notifications
|
|
|
66
66
|
nothing. It names no count, no document and no source: all three are
|
|
67
67
|
scope-gated, and a frozen sentence cannot be taken back.
|
|
68
68
|
`../__tests__/vocabulary.test.ts` enforces "no digit" for both checkpoints.
|
|
69
|
+
- **`workflow-needs-attention.ts` FRAMES; IT DOES NOT NARRATE.** Its `text` is
|
|
70
|
+
the backend's settlement narration verbatim (one owner of settlement wording),
|
|
71
|
+
its title is per workflow type with no digit and no next step, it has no
|
|
72
|
+
chips, and it carries the fact-free outcome as `outcome` so the outcome's
|
|
73
|
+
grounded affordances render as real controls (control ADR
|
|
74
|
+
`affordance-system`). `../__tests__/workflow-attention.test.ts` pins all of
|
|
75
|
+
it.
|
|
69
76
|
|
|
70
77
|
## Public API
|
|
71
78
|
|
|
@@ -80,10 +87,11 @@ chat. That is the precedent notify/'s README states for outbound notifications
|
|
|
80
87
|
| `billingSuspendedComposer` | The grace period elapsed; leads with "nothing was deleted" |
|
|
81
88
|
| `billingSubscriptionCanceledComposer` | Seats ended; AI funds are not forfeit |
|
|
82
89
|
| `sourceSyncReviewReadyComposer` | A source review waits; nothing has happened yet; no count |
|
|
90
|
+
| `workflowNeedsAttentionComposer` | A background run failed; frames the backend narration, no chips |
|
|
83
91
|
|
|
84
92
|
## Dependencies
|
|
85
93
|
|
|
86
94
|
Type-only imports of `ProactiveChatComposer` (`../composer`),
|
|
87
95
|
`ProactiveChatTemplate` (`../surfaces`) and the per-template facts
|
|
88
|
-
(`../facts`). No `zod`, no runtime imports beyond the sibling template
|
|
96
|
+
(`../facts`), and of `WorkflowType` (`../../workflows`). No `zod`, no runtime imports beyond the sibling template
|
|
89
97
|
modules.
|