@company-semantics/contracts 89.2.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.
Files changed (53) hide show
  1. package/package.json +4 -3
  2. package/src/affordances/README.md +65 -0
  3. package/src/affordances/__tests__/README.md +30 -0
  4. package/src/affordances/__tests__/affordances.test.ts +251 -0
  5. package/src/affordances/affordance.ts +124 -0
  6. package/src/affordances/args.ts +59 -0
  7. package/src/affordances/availability.ts +78 -0
  8. package/src/affordances/ids.ts +45 -0
  9. package/src/affordances/index.ts +39 -0
  10. package/src/api/generated-spec-hash.ts +2 -2
  11. package/src/api/generated.ts +142 -2
  12. package/src/generated/openapi-routes.ts +1 -0
  13. package/src/proactive/README.md +26 -4
  14. package/src/proactive/__tests__/README.md +10 -0
  15. package/src/proactive/__tests__/chat-templates.test.ts +8 -0
  16. package/src/proactive/__tests__/vocabulary.test.ts +11 -0
  17. package/src/proactive/__tests__/workflow-attention-fixtures.ts +49 -0
  18. package/src/proactive/__tests__/workflow-attention.test.ts +146 -0
  19. package/src/proactive/composer.ts +8 -0
  20. package/src/proactive/facts.ts +36 -0
  21. package/src/proactive/index.ts +2 -0
  22. package/src/proactive/kinds.ts +29 -0
  23. package/src/proactive/registry.ts +22 -0
  24. package/src/proactive/surfaces.ts +2 -0
  25. package/src/proactive/templates/README.md +9 -1
  26. package/src/proactive/templates/index.ts +2 -0
  27. package/src/proactive/templates/workflow-needs-attention.ts +35 -0
  28. package/src/root-exports/README.md +1 -0
  29. package/src/root-exports/delivery.ts +69 -1
  30. package/src/source-sync/README.md +95 -5
  31. package/src/source-sync/__tests__/attention-evidence.test.ts +96 -0
  32. package/src/source-sync/__tests__/barrel.test.ts +39 -0
  33. package/src/source-sync/__tests__/enumeration.test.ts +160 -0
  34. package/src/source-sync/__tests__/schemas.test.ts +223 -0
  35. package/src/source-sync/__tests__/vocabulary.test.ts +104 -5
  36. package/src/source-sync/attention-evidence.ts +93 -0
  37. package/src/source-sync/candidate-item.ts +25 -0
  38. package/src/source-sync/enumeration.ts +152 -0
  39. package/src/source-sync/index.ts +35 -1
  40. package/src/source-sync/relevance.ts +85 -11
  41. package/src/source-sync/vocabulary.ts +37 -1
  42. package/src/user-notifications/README.md +8 -0
  43. package/src/user-notifications/kinds.ts +13 -0
  44. package/src/user-notifications/lifecycle-registry.ts +10 -0
  45. package/src/workflows/README.md +38 -2
  46. package/src/workflows/__tests__/README.md +21 -2
  47. package/src/workflows/__tests__/affordances-of-outcome.test.ts +99 -0
  48. package/src/workflows/__tests__/deferred-route.test.ts +168 -0
  49. package/src/workflows/__tests__/outcome.test.ts +66 -0
  50. package/src/workflows/affordances-of-outcome.ts +65 -0
  51. package/src/workflows/deferred-route.ts +240 -0
  52. package/src/workflows/index.ts +34 -0
  53. package/src/workflows/outcome.ts +114 -1
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The affordance vocabulary: every next step the product may OFFER a person
3
+ * or the agent (control ADR affordance-system).
4
+ *
5
+ * An affordance names an action the product can actually perform. Existing
6
+ * here is never a reason to offer it: an affordance is offered only by a
7
+ * domain-owned, deterministic offer rule that relates a domain state to it
8
+ * (INV-AFFORD-RELEVANT). Neither a model nor a renderer invents, infers or
9
+ * searches for one.
10
+ *
11
+ * INVARIANTS:
12
+ * - `{domain}.{action}` names, camelCase after the dot. A member is persisted
13
+ * inside immutable workflow outcomes and chat parts, so the set is CLOSED and
14
+ * append-only: removing one orphans every record that carries it.
15
+ * - GROUNDED. Every member has a binding kind (`AFFORDANCE_BINDING_KIND_OF` in
16
+ * ./affordance) and the repo that owns the binding proves it exists with an
17
+ * architecture test: a mounted backend route for `command`, an app `Route`
18
+ * for `navigate`, an app launch surface for `surface` (INV-AFFORD-GROUNDED).
19
+ */
20
+ import { z } from "zod";
21
+
22
+ export const AFFORDANCE_IDS = [
23
+ /** Start a new search of the connected drive (Source Sync discovery). */
24
+ "sourceSync.findFiles",
25
+ /** Connect Google Drive, or reconnect it when its sign-in stopped working. */
26
+ "integrations.connectGoogleDrive",
27
+ /** Turn Source Sync on for the org. */
28
+ "sourceSync.enable",
29
+ /** Open the org's billing page. */
30
+ "billing.open",
31
+ /** Run the org-structure analysis again. */
32
+ "orgStructure.reanalyze",
33
+ /** Sync the HR directory again. */
34
+ "hris.resyncDirectory",
35
+ /**
36
+ * Open a review (an input request) in chat. PROJECTED, never persisted in an
37
+ * offer: it is derived from authoritative state already stored elsewhere (a
38
+ * workflow outcome's `review` disposition). See PROJECTED_AFFORDANCE_IDS.
39
+ */
40
+ "review.open",
41
+ ] as const;
42
+
43
+ export type AffordanceId = (typeof AFFORDANCE_IDS)[number];
44
+
45
+ export const AffordanceIdSchema = z.enum(AFFORDANCE_IDS);
@@ -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 = '7144c9a832d5' as const;
3
- export const SPEC_HASH_FULL = '7144c9a832d55cf9179e5825c6c8fa486bf7130c0d1022d1b721f175dcf99f6d' as const;
2
+ export const SPEC_HASH = 'b2a6eec5dea4' as const;
3
+ export const SPEC_HASH_FULL = 'b2a6eec5dea4f72290740a1cf16608a826ece2e3654ba64d2bc5e3794d571809' as const;
@@ -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";
@@ -9328,8 +9345,10 @@ export interface components {
9328
9345
  /** @enum {string} */
9329
9346
  classificationState: "unclassified" | "classified";
9330
9347
  sourceClass: ("board_deck" | "board_minutes" | "investor_update" | "financial_brief" | "budget_forecast" | "okr_plan" | "annual_plan" | "all_hands" | "unit_strategy" | "roadmap" | "qbr" | "other" | "decoy") | null;
9331
- documentType: ("board_deck" | "board_minutes" | "strategy_memo" | "planning_doc" | "okr_doc" | "roadmap" | "operating_review" | "all_hands_deck" | "offsite_notes" | "investor_update" | "pitch_deck" | "product_spec" | "research_analysis" | "general_meeting_notes" | "financial_plan" | "financial_actuals" | "equity_admin_record" | "corporate_formation_record" | "corporate_governance_record" | "government_filing" | "equity_agreement" | "commercial_agreement" | "employment_agreement" | "transaction_record" | "personal_document" | "social_event_material" | "coursework" | "empty_template" | "image_or_screenshot" | "unknown") | null;
9348
+ documentType: ("board_deck" | "board_minutes" | "strategy_memo" | "planning_doc" | "okr_doc" | "roadmap" | "operating_review" | "all_hands_deck" | "offsite_notes" | "investor_update" | "pitch_deck" | "product_spec" | "research_analysis" | "general_meeting_notes" | "financial_plan" | "financial_actuals" | "equity_admin_record" | "corporate_formation_record" | "corporate_governance_record" | "government_filing" | "equity_agreement" | "commercial_agreement" | "employment_agreement" | "transaction_record" | "personal_document" | "social_event_material" | "coursework" | "empty_template" | "image_or_screenshot" | "team_charter" | "process_playbook" | "policy_handbook" | "onboarding_guide" | "values_culture" | "positioning_messaging" | "decision_log" | "postmortem" | "metrics_definition" | "operating_cadence" | "unknown") | null;
9332
9349
  documentTypeConfidence: number | null;
9350
+ operatingKnowledgeFamily: ("direction" | "operating_model" | "practices" | "identity_market" | "decisions_learning") | null;
9351
+ canonicalPath: string | null;
9333
9352
  periodLabel: string | null;
9334
9353
  seriesRole: ("current" | "history") | null;
9335
9354
  relevance: number;
@@ -9375,6 +9394,59 @@ export interface components {
9375
9394
  t1: ("exploit" | "explore") | null;
9376
9395
  t2: ("exploit" | "explore") | null;
9377
9396
  };
9397
+ attentionEvidence: ({
9398
+ /** @constant */
9399
+ kind: "tree_prior";
9400
+ score: number;
9401
+ folderId: string;
9402
+ source: {
9403
+ producer: string;
9404
+ version: string;
9405
+ };
9406
+ } | {
9407
+ /** @constant */
9408
+ kind: "path_terms";
9409
+ values: string[];
9410
+ source: {
9411
+ producer: string;
9412
+ version: string;
9413
+ };
9414
+ } | {
9415
+ /** @constant */
9416
+ kind: "content_match";
9417
+ values: string[];
9418
+ strength: number;
9419
+ probeId: string;
9420
+ source: {
9421
+ producer: string;
9422
+ version: string;
9423
+ };
9424
+ } | {
9425
+ /** @constant */
9426
+ kind: "breadth";
9427
+ /** @enum {string} */
9428
+ value: "restricted_few" | "group" | "domain" | "anyone" | "unknown";
9429
+ source: {
9430
+ producer: string;
9431
+ version: string;
9432
+ };
9433
+ } | {
9434
+ /** @constant */
9435
+ kind: "recency";
9436
+ score: number;
9437
+ source: {
9438
+ producer: string;
9439
+ version: string;
9440
+ };
9441
+ } | {
9442
+ /** @constant */
9443
+ kind: "mime_prior";
9444
+ score: number;
9445
+ source: {
9446
+ producer: string;
9447
+ version: string;
9448
+ };
9449
+ })[] | null;
9378
9450
  included: boolean;
9379
9451
  treatmentUnresolved: boolean;
9380
9452
  matchReason: string;
@@ -9465,6 +9537,33 @@ export interface components {
9465
9537
  coveragePercent: number;
9466
9538
  /** @enum {string} */
9467
9539
  scanOutcome: "complete" | "budget_exhausted" | "file_ceiling" | "stopped";
9540
+ enumerationCompleteness: ("complete" | "local_budget_truncated" | "provider_incomplete" | "both") | null;
9541
+ rootCoverage: {
9542
+ /** @enum {string} */
9543
+ rootKind: "user" | "shared_drive" | "domain";
9544
+ rootId: string | null;
9545
+ /** @enum {string} */
9546
+ status: "complete" | "local_budget_truncated" | "provider_incomplete" | "both";
9547
+ enumeratedCount: number;
9548
+ observedOldestModifiedTime: string | null;
9549
+ observedNewestModifiedTime: string | null;
9550
+ skippedRanges: {
9551
+ /** Format: date-time */
9552
+ from: string;
9553
+ /** Format: date-time */
9554
+ to: string;
9555
+ }[];
9556
+ }[] | null;
9557
+ stageCounters: {
9558
+ filesEnumerated: number;
9559
+ filesMetadataEligible: number;
9560
+ filesT0Scored: number;
9561
+ filesT1Attended: number;
9562
+ filesPeeked: number;
9563
+ filesClassified: number;
9564
+ filesShown: number;
9565
+ filesRetained: number;
9566
+ } | null;
9468
9567
  } | null;
9469
9568
  };
9470
9569
  SourceSyncApproveResponse: {
@@ -17982,6 +18081,47 @@ export interface operations {
17982
18081
  };
17983
18082
  };
17984
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
+ };
17985
18125
  listCommentThreads: {
17986
18126
  parameters: {
17987
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'],
@@ -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 (8: 2 + 5 `billing*` + 1 `source*`) |
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 (3 org/brief/chat + 15 `billing.*` + 2 `sourceSync.*`) |
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
- | `ProactiveChatMessage` | What a composer returns: `title`, `text`, and 0..4 `replies` chips |
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. No `zod`, no runtime imports; pure vocabulary
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
  // =============================================================================