@company-semantics/contracts 58.0.0 → 58.2.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 (55) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/resource-keys.test.ts +30 -0
  3. package/src/api/generated-spec-hash.ts +2 -2
  4. package/src/api/generated.ts +33 -1
  5. package/src/chat/README.md +15 -4
  6. package/src/chat/__tests__/proactive-kind.test.ts +51 -0
  7. package/src/chat/index.ts +9 -0
  8. package/src/chat/proactive-kind.ts +51 -0
  9. package/src/chat/schemas.ts +92 -1
  10. package/src/chat/types.ts +19 -1
  11. package/src/index.ts +110 -0
  12. package/src/message-parts/README.md +5 -0
  13. package/src/message-parts/__tests__/suggested-replies.test.ts +52 -0
  14. package/src/message-parts/__tests__/wire.test.ts +48 -0
  15. package/src/message-parts/index.ts +8 -0
  16. package/src/message-parts/suggested-replies.ts +48 -0
  17. package/src/message-parts/types.ts +7 -1
  18. package/src/message-parts/wire.ts +26 -0
  19. package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +1 -0
  20. package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +1 -0
  21. package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +207 -0
  22. package/src/notifications/__tests__/fixtures.ts +9 -0
  23. package/src/notifications/__tests__/org-invite.test.ts +75 -0
  24. package/src/notifications/__tests__/render-snapshot.test.ts +8 -0
  25. package/src/notifications/kinds/org-invite.ts +27 -12
  26. package/src/notifications/payloads.ts +7 -0
  27. package/src/org/README.md +38 -0
  28. package/src/org/__tests__/canonical-facts.test.ts +118 -0
  29. package/src/org/__tests__/structure-inference.test.ts +392 -0
  30. package/src/org/__tests__/structure-provenance.test.ts +187 -0
  31. package/src/org/canonical-facts.ts +94 -1
  32. package/src/org/index.ts +54 -0
  33. package/src/org/schemas.ts +23 -0
  34. package/src/org/structure-inference.ts +521 -0
  35. package/src/proactive/README.md +125 -0
  36. package/src/proactive/__tests__/README.md +56 -0
  37. package/src/proactive/__tests__/chat-templates.test.ts +167 -0
  38. package/src/proactive/__tests__/compile-fixtures.ts +110 -0
  39. package/src/proactive/__tests__/vocabulary.test.ts +279 -0
  40. package/src/proactive/classes.ts +125 -0
  41. package/src/proactive/composer.ts +104 -0
  42. package/src/proactive/facts.ts +87 -0
  43. package/src/proactive/index.ts +52 -0
  44. package/src/proactive/kinds.ts +127 -0
  45. package/src/proactive/plan.ts +79 -0
  46. package/src/proactive/registry.ts +71 -0
  47. package/src/proactive/surfaces.ts +59 -0
  48. package/src/proactive/templates/README.md +58 -0
  49. package/src/proactive/templates/index.ts +32 -0
  50. package/src/proactive/templates/morning-brief.ts +77 -0
  51. package/src/proactive/templates/org-became-shared.ts +54 -0
  52. package/src/resource-key-types.ts +9 -0
  53. package/src/resource-keys.ts +2 -0
  54. package/src/user-notifications/README.md +10 -0
  55. package/src/user-notifications/kinds.ts +28 -0
package/src/index.ts CHANGED
@@ -324,6 +324,49 @@ export type {
324
324
  UserNotificationKind,
325
325
  } from "./user-notifications/index";
326
326
 
327
+ // Proactive delivery — how the system speaks first (ADR-CONTRACTS-142). A
328
+ // COMPOSER above the three notification vocabularies, not a fourth peer: the
329
+ // presentation class is a TOTAL FUNCTION from class to surfaces
330
+ // (CLASS_SURFACES, checked in both directions by the compiler), and a kind
331
+ // carries its class plus the IDS each surface needs — which banner, which chat
332
+ // template, which inbox kind — never the surface booleans. The born-read rule
333
+ // is stated once (bornRead) so the backend imports it rather than re-deriving
334
+ // it per call site. Pushed-chat prose lives here too (PROACTIVE_CHAT_COMPOSERS,
335
+ // total over the templates): the backend hands a composer facts and writes what
336
+ // comes back, FROZEN into the transcript — so a composer may only interpolate
337
+ // facts the recipient was entitled to at compose time. See src/proactive/README.md.
338
+ export {
339
+ PROACTIVE_PRESENTATION_CLASSES,
340
+ CLASS_SURFACES,
341
+ bornRead,
342
+ ORG_SYSTEM_EVENT_TYPES,
343
+ PROACTIVE_CHAT_TEMPLATES,
344
+ PROACTIVE_EVENT_KIND_IDS,
345
+ PROACTIVE_RECIPIENT_REASONS,
346
+ PROACTIVE_PROJECTIONS,
347
+ PROACTIVE_EVENT_KINDS,
348
+ PROACTIVE_CHAT_MAX_REPLIES,
349
+ PROACTIVE_CHAT_COMPOSERS,
350
+ } from "./proactive/index";
351
+
352
+ export type {
353
+ ProactivePresentationClass,
354
+ BadgeCarrier,
355
+ ClassSurfacePlan,
356
+ OrgSystemEventType,
357
+ ProactiveChatTemplate,
358
+ ProactiveEventKind,
359
+ ProactiveEventId,
360
+ ProactiveRecipientReason,
361
+ ProactiveProjection,
362
+ ProactiveEventDefinition,
363
+ OrgBecameSharedFacts,
364
+ MorningBriefFacts,
365
+ ProactiveChatFacts,
366
+ ProactiveChatComposer,
367
+ ProactiveChatMessage,
368
+ } from "./proactive/index";
369
+
327
370
  // Comment wire contract — where a thread hangs (the anchor union), what a
328
371
  // thread and its comments look like on the wire, and who a comment may name.
329
372
  // The anchor is the single most load-bearing shape in this package: the backend
@@ -481,11 +524,21 @@ export type {
481
524
  // Runtime profile types (PRD-00229)
482
525
  ChatRuntimeProfile,
483
526
  ChatRuntimeProfileInfo,
527
+ // Proactive chat wire vocabulary (ADR-CONTRACTS-142): how a chat was born,
528
+ // the occurrence-to-chat resolution states, and the recognized form of the
529
+ // wire-lenient `proactiveKind` string
530
+ ChatOrigin,
531
+ ProactiveChatResolution,
532
+ RecognizedProactiveKind,
484
533
  } from "./chat/index";
485
534
 
486
535
  export {
487
536
  CHAT_RUNTIME_PROFILES,
488
537
  DEFAULT_CHAT_RUNTIME_PROFILE,
538
+ // `proactiveKind` is a bare string on the wire so a new kind cannot make an
539
+ // older client reject the whole chat list; this is the ONE place it becomes
540
+ // a ProactiveEventKind. See src/chat/proactive-kind.ts.
541
+ recognizeProactiveKind,
489
542
  } from "./chat/index";
490
543
 
491
544
  // Organization domain types
@@ -798,20 +851,72 @@ export type {
798
851
  export {
799
852
  CanonicalPersonSchema,
800
853
  CanonicalOrgUnitSchema,
854
+ CanonicalPositionStatusSchema,
801
855
  CanonicalPositionSchema,
802
856
  CanonicalOccupancySchema,
803
857
  CanonicalReportingEdgeSchema,
858
+ CanonicalUnresolvedManagerResolutionSchema,
859
+ CanonicalUnresolvedManagerSchema,
804
860
  CanonicalFactsSchema,
805
861
  } from "./org/index";
806
862
  export type {
807
863
  CanonicalPerson,
808
864
  CanonicalOrgUnit,
865
+ CanonicalPositionStatus,
809
866
  CanonicalPosition,
810
867
  CanonicalOccupancy,
811
868
  CanonicalReportingEdge,
869
+ CanonicalUnresolvedManagerResolution,
870
+ CanonicalUnresolvedManager,
812
871
  CanonicalFacts,
813
872
  } from "./org/index";
814
873
 
874
+ // Org-structure inference: a StructureInferenceSnapshot of the persisted graph
875
+ // goes in, a StructureProposal — a proposed desired state, never database
876
+ // commands — comes out. Typed evidence, targetUnitId for durable identity, and
877
+ // exactly one PersonStructureOutcome per person in the snapshot. (PRD-00955)
878
+ //
879
+ // Acceptance attaches StructureProvenance — proposalOrigin, acceptedBy and
880
+ // authority as three ORTHOGONAL axes, so accepting an AI proposal never rewrites
881
+ // its origin as manual. mayOverrideAuthority is the single definition of
882
+ // precedence, and it consults `authority` only. (PRD-00956)
883
+ export {
884
+ StructureEvidenceSchema,
885
+ TOPOLOGY_ONLY_EVIDENCE_KINDS,
886
+ StructureUnitAuthoritySchema,
887
+ StructureProposalOriginSchema,
888
+ StructureAcceptedBySchema,
889
+ StructureAuthoritySchema,
890
+ StructureProvenanceSchema,
891
+ mayOverrideAuthority,
892
+ ProposedOrgUnitSchema,
893
+ PersonStructureOutcomeSchema,
894
+ StructureReviewItemSchema,
895
+ StructureProposalSchema,
896
+ StructurePersonFactSchema,
897
+ StructureReportingFactSchema,
898
+ ExistingUnitFactSchema,
899
+ ExistingPlacementFactSchema,
900
+ StructureInferenceSnapshotSchema,
901
+ } from "./org/index";
902
+ export type {
903
+ StructureEvidence,
904
+ StructureUnitAuthority,
905
+ StructureProposalOrigin,
906
+ StructureAcceptedBy,
907
+ StructureAuthority,
908
+ StructureProvenance,
909
+ ProposedOrgUnit,
910
+ PersonStructureOutcome,
911
+ StructureReviewItem,
912
+ StructureProposal,
913
+ StructurePersonFact,
914
+ StructureReportingFact,
915
+ ExistingUnitFact,
916
+ ExistingPlacementFact,
917
+ StructureInferenceSnapshot,
918
+ } from "./org/index";
919
+
815
920
  // Authority & Delegation vocabulary
816
921
  export {
817
922
  AuthoritySourceSchema,
@@ -909,6 +1014,11 @@ export type {
909
1014
  InteractiveTaskData,
910
1015
  InteractiveTaskPart,
911
1016
  InteractiveTaskDataPart,
1017
+ // Suggested replies surface types (non-governed chips, PRD-00958)
1018
+ SuggestedReply,
1019
+ SuggestedRepliesData,
1020
+ SuggestedRepliesPart,
1021
+ SuggestedRepliesDataPart,
912
1022
  // Execution result types (Phase 5)
913
1023
  ExecutionArtifactStatus,
914
1024
  ExecutionResultSummary,
@@ -14,6 +14,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
14
14
  - Confirmation is deterministic — no natural language inference ("yes" is invalid)
15
15
  - Execution results carry `state` resolved from ExecutionState (single authority, no redundant status)
16
16
  - Undo creates append-only audit rows — original execution is never mutated
17
+ - `SuggestedRepliesPart` is a surface but NOT a governed one: a chip fires an ordinary user turn (no `executionId`, no `submitEndpoint`), is stateless (clicking never consumes it) and advisory (the composer stays enabled) — so it is outside the at-most-one-governed-surface-per-turn rule
17
18
 
18
19
  <!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
19
20
 
@@ -65,6 +66,10 @@ Canonical vocabulary for structured assistant message output. Defines the type s
65
66
  - `StatusPanelEntry` _(type)_ — Single entry in a status panel.
66
67
  - `StatusPanelPart` _(type)_ — Status panel surface part.
67
68
  - `StreamPhase` _(type)_ — Derived stream phase for UI state management.
69
+ - `SuggestedRepliesData` _(type)_ — Suggested replies surface data payload.
70
+ - `SuggestedRepliesDataPart` _(type)_ — Wire form, as persisted in `chat_messages.parts`.
71
+ - `SuggestedRepliesPart` _(type)_ — Normalized form, as rendered.
72
+ - `SuggestedReply` _(type)_ — Suggested follow-up chips beneath an assistant turn.
68
73
  - `SurfacePart` _(type)_ — Surface parts are rendered atomically (never streamed).
69
74
  - `TablePart` _(type)_ — Table surface part.
70
75
  - `TextPart` _(type)_ — Text content part.
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { isSurfacePart, isTextPart } from "../types.js";
3
+ import type { AssistantMessagePart, SurfacePart } from "../types.js";
4
+ import { createPartBuilder, addSurface, addText } from "../builder.js";
5
+ import type { SuggestedRepliesPart } from "../suggested-replies.js";
6
+
7
+ // The semantic part is what the app renders after normalizing the wire part.
8
+ // Typing the fixture as SuggestedRepliesPart and then assigning it to
9
+ // SurfacePart is the compile-time proof that the union gained the member.
10
+ const chips: SuggestedRepliesPart = {
11
+ type: "suggested-replies",
12
+ data: {
13
+ replies: [
14
+ { id: "r1", label: "What moved?" },
15
+ { id: "r2", label: "Undo", prompt: "Undo the last change" },
16
+ ],
17
+ },
18
+ };
19
+
20
+ describe("SuggestedRepliesPart in the SurfacePart union", () => {
21
+ it("is assignable to SurfacePart and AssistantMessagePart", () => {
22
+ const asSurface: SurfacePart = chips;
23
+ const asPart: AssistantMessagePart = chips;
24
+ expect(asSurface.type).toBe("suggested-replies");
25
+ expect(asPart.type).toBe("suggested-replies");
26
+ });
27
+
28
+ it("is classified as a surface part, not a text part", () => {
29
+ expect(isSurfacePart(chips)).toBe(true);
30
+ expect(isTextPart(chips)).toBe(false);
31
+ });
32
+
33
+ // The builder's narrative-before-surface state machine applies to chips like
34
+ // any other surface: they are emitted after the prose, never streamed.
35
+ it("is accepted by addSurface after narrative text", () => {
36
+ let state = createPartBuilder();
37
+ state = addText(state, "Five settings sections moved.").state;
38
+ const result = addSurface(state, chips);
39
+ expect(result.accepted).toBe(true);
40
+ expect(result.state.parts).toEqual([
41
+ { type: "text", text: "Five settings sections moved." },
42
+ chips,
43
+ ]);
44
+ });
45
+
46
+ it("does not consume: the same part can be added to two builders unchanged", () => {
47
+ const a = addSurface(createPartBuilder(), chips).state.parts[0];
48
+ const b = addSurface(createPartBuilder(), chips).state.parts[0];
49
+ expect(a).toEqual(b);
50
+ expect(a).toEqual(chips);
51
+ });
52
+ });
@@ -3,6 +3,7 @@ import { WireSurfaceBuilder } from "../wire.js";
3
3
  import type { MCPToolDescriptor } from "../../mcp/index.js";
4
4
  import type { ExecutionResultData } from "../execution.js";
5
5
  import type { UndoResultData } from "../execution.js";
6
+ import type { SuggestedRepliesData } from "../suggested-replies.js";
6
7
 
7
8
  // Deliberately partial fixtures: toolList is a pure wrapper that passes `tools`
8
9
  // straight through, so these assert the wrapping, not descriptor validity. Cast
@@ -90,6 +91,53 @@ describe("WireSurfaceBuilder.undoResult", () => {
90
91
  });
91
92
  });
92
93
 
94
+ describe("WireSurfaceBuilder.suggestedReplies", () => {
95
+ const data: SuggestedRepliesData = {
96
+ replies: [
97
+ { id: "r1", label: "Show me what moved" },
98
+ {
99
+ id: "r2",
100
+ label: "Why?",
101
+ prompt: "Why did these settings move out of My settings?",
102
+ },
103
+ ],
104
+ };
105
+
106
+ it("returns data part with type data-suggested-replies", () => {
107
+ const result = WireSurfaceBuilder.suggestedReplies(data);
108
+ expect(result.type).toBe("data-suggested-replies");
109
+ });
110
+
111
+ it("data matches input SuggestedRepliesData exactly", () => {
112
+ const result = WireSurfaceBuilder.suggestedReplies(data);
113
+ expect(result.data).toEqual(data);
114
+ });
115
+
116
+ // A chip is an ordinary user turn: the part carries nothing that would make
117
+ // it a governed surface. Pinning the key set catches an accidental
118
+ // executionId/submitEndpoint creeping onto the payload.
119
+ it("carries no governed-surface fields (no executionId, no submitEndpoint)", () => {
120
+ const result = WireSurfaceBuilder.suggestedReplies(data);
121
+ expect(Object.keys(result.data)).toEqual(["replies"]);
122
+ expect(result.data).not.toHaveProperty("executionId");
123
+ expect(result.data).not.toHaveProperty("submitEndpoint");
124
+ });
125
+
126
+ // Stateless: building twice from the same facts yields identical output, so
127
+ // a historical message re-rendered later cannot differ from the original.
128
+ it("is deterministic across repeated builds", () => {
129
+ expect(WireSurfaceBuilder.suggestedReplies(data)).toEqual(
130
+ WireSurfaceBuilder.suggestedReplies(data),
131
+ );
132
+ });
133
+
134
+ it("works with an empty replies array", () => {
135
+ const result = WireSurfaceBuilder.suggestedReplies({ replies: [] });
136
+ expect(result.type).toBe("data-suggested-replies");
137
+ expect(result.data.replies).toEqual([]);
138
+ });
139
+ });
140
+
93
141
  describe("WireSurfaceBuilder.messageStart", () => {
94
142
  it("returns data part with type data-message-start", () => {
95
143
  const result = WireSurfaceBuilder.messageStart("msg-123");
@@ -56,6 +56,14 @@ export type {
56
56
  InteractiveTaskDataPart,
57
57
  } from "./interactive";
58
58
 
59
+ // Suggested replies surface types (non-governed chips)
60
+ export type {
61
+ SuggestedReply,
62
+ SuggestedRepliesData,
63
+ SuggestedRepliesPart,
64
+ SuggestedRepliesDataPart,
65
+ } from "./suggested-replies";
66
+
59
67
  // Execution result types
60
68
  export type {
61
69
  ExecutionArtifactStatus,
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Suggested follow-up chips beneath an assistant turn.
3
+ *
4
+ * INVARIANTS — these are what keep this OUT of the governed
5
+ * preview/confirmation/interactive triad:
6
+ *
7
+ * - A chip fires an ORDINARY USER TURN. No `executionId`, no `submitEndpoint`,
8
+ * no side effect of its own. That is precisely why it is not a governed
9
+ * surface and is not subject to the at-most-one-governed-surface-per-turn
10
+ * rule.
11
+ * - STATELESS. Clicking does not consume it; the historical message renders
12
+ * identically forever, because the turn it produced is already in the
13
+ * transcript below it.
14
+ * - ADVISORY and non-exhaustive. The composer box stays enabled.
15
+ *
16
+ * Purely additive: an app that predates this part normalizes unknown part
17
+ * types to null, so it drops the chips and still renders the prose.
18
+ *
19
+ * @see decisions/ADR-CONT-026.md for the message-parts design
20
+ * @see decisions/ADR-CONTRACTS-142-proactive-vocabulary-composes-the-three-notification-unions.md
21
+ */
22
+ export interface SuggestedReply {
23
+ /** Stable within the part. Telemetry key and React key; never a server id. */
24
+ readonly id: string;
25
+ /** What the chip SAYS. */
26
+ readonly label: string;
27
+ /** What the chip SENDS, when it differs from the label. */
28
+ readonly prompt?: string;
29
+ }
30
+
31
+ /**
32
+ * Suggested replies surface data payload.
33
+ */
34
+ export interface SuggestedRepliesData {
35
+ readonly replies: readonly SuggestedReply[];
36
+ }
37
+
38
+ /** Normalized form, as rendered. */
39
+ export interface SuggestedRepliesPart {
40
+ type: "suggested-replies";
41
+ data: SuggestedRepliesData;
42
+ }
43
+
44
+ /** Wire form, as persisted in `chat_messages.parts`. */
45
+ export interface SuggestedRepliesDataPart {
46
+ type: "data-suggested-replies";
47
+ data: SuggestedRepliesData;
48
+ }
@@ -17,6 +17,7 @@ import type { ToolListMessagePart } from "../mcp/index";
17
17
  import type { PreviewPart } from "./preview";
18
18
  import type { ConfirmationPart } from "./confirmation";
19
19
  import type { InteractiveTaskPart } from "./interactive";
20
+ import type { SuggestedRepliesPart } from "./suggested-replies";
20
21
 
21
22
  // =============================================================================
22
23
  // Narrative Parts (Streamable)
@@ -112,6 +113,10 @@ export interface TablePart {
112
113
  /**
113
114
  * Surface parts are rendered atomically (never streamed).
114
115
  * Extensible: add new surface types to this union.
116
+ *
117
+ * SuggestedRepliesPart is a surface part but NOT a governed one: a chip fires
118
+ * an ordinary user turn, so it is outside the preview/confirmation/interactive
119
+ * triad and its at-most-one-governed-surface-per-turn rule.
115
120
  */
116
121
  export type SurfacePart =
117
122
  | ToolListPart
@@ -120,7 +125,8 @@ export type SurfacePart =
120
125
  | TablePart
121
126
  | ConfirmationPart
122
127
  | PreviewPart
123
- | InteractiveTaskPart;
128
+ | InteractiveTaskPart
129
+ | SuggestedRepliesPart;
124
130
 
125
131
  /**
126
132
  * All assistant message part types.
@@ -17,6 +17,10 @@ import type {
17
17
  InteractiveTaskData,
18
18
  InteractiveTaskDataPart,
19
19
  } from "./interactive";
20
+ import type {
21
+ SuggestedRepliesData,
22
+ SuggestedRepliesDataPart,
23
+ } from "./suggested-replies";
20
24
  import type {
21
25
  ExecutionResultData,
22
26
  ExecutionResultDataPart,
@@ -112,6 +116,28 @@ export const WireSurfaceBuilder = {
112
116
  };
113
117
  },
114
118
 
119
+ /**
120
+ * Build a suggested-replies data part for streaming.
121
+ * Chips beneath an assistant turn that each fire an ORDINARY user turn.
122
+ *
123
+ * INVARIANTS:
124
+ * - Not a governed surface: no executionId, no submitEndpoint, no side
125
+ * effect of its own — so it does not count against the
126
+ * at-most-one-governed-surface-per-turn rule
127
+ * - Stateless: clicking does not consume it; the historical message renders
128
+ * identically forever
129
+ * - Advisory and non-exhaustive: the composer box stays enabled
130
+ *
131
+ * @param data - Suggested replies data (the chip list)
132
+ * @returns Wire-format suggested replies part ready for stream
133
+ */
134
+ suggestedReplies(data: SuggestedRepliesData): SuggestedRepliesDataPart {
135
+ return {
136
+ type: "data-suggested-replies",
137
+ data,
138
+ };
139
+ },
140
+
115
141
  /**
116
142
  * Build an execution result data part for streaming.
117
143
  * Reports the outcome of an executed action.
@@ -11,6 +11,7 @@ exports[`monospace budget > overhangs only where content is atomic 1`] = `
11
11
  "https://app.companysemantics.ai/doc/handbook?thread=th_456",
12
12
  "https://app.companysemantics.ai/invite/abc123",
13
13
  "https://app.companysemantics.ai/invite/def456",
14
+ "https://app.companysemantics.ai/invite/ghi789",
14
15
  "https://app.companysemantics.ai/meeting/8842",
15
16
  "https://app.companysemantics.ai/org/unit/42",
16
17
  "https://app.companysemantics.ai/owner-transfer/accept/tok123",
@@ -24,6 +24,7 @@ exports[`NOTIFICATION_DEFINITIONS > titles every kind 1`] = `
24
24
  "companyMd.access_requested · With message": "Someone requested access to a document",
25
25
  "org.invite · Admin": "You've been invited to join Acme Corp on Company Semantics",
26
26
  "org.invite · Member": "You've been invited to join Acme Corp on Company Semantics",
27
+ "org.invite · Member + note": "You've been invited to join Acme Corp on Company Semantics",
27
28
  "org.ownership_transfer · No note (admin)": "You've been invited to become a workspace owner",
28
29
  "org.ownership_transfer · With note + from": "You've been invited to become a workspace owner",
29
30
  "org.ownership_transfer_completed · Default": "Workspace ownership has been transferred",
@@ -3481,6 +3481,213 @@ If you weren't expecting this, no action is
3481
3481
  required.
3482
3482
 
3483
3483
 
3484
+ /* EOM */
3485
+ ⓒ 2026 • Company Semantics
3486
+ https://companysemantics.ai
3487
+ ",
3488
+ }
3489
+ `;
3490
+
3491
+ exports[`email render > org.invite · Member + note 1`] = `
3492
+ {
3493
+ "amp": "<!DOCTYPE html>
3494
+ <html amp4email data-css-strict lang="en">
3495
+ <head><meta charset="utf-8">
3496
+ <script async src="https://cdn.ampproject.org/v0.js"></script>
3497
+ <style amp4email-boilerplate>body{visibility:hidden}</style>
3498
+ <style amp-custom>.csr-body { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; background-color: #ffffff; margin: 0; padding: 0; }
3499
+ .csr-frame { max-width: 520px; margin: 0 auto; }
3500
+ .csr-p-normal { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0; }
3501
+ .csr-p-tight { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0; }
3502
+ .csr-p-none { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0; }
3503
+ .csr-eom { color: #bbb; }
3504
+ .csr-signature-link { color: #0047FF; text-decoration: none; }
3505
+ .csr-warning { color: #e7000b; }
3506
+ .csr-hr-normal { border: none; border-top: 1px solid #bbb; margin: 0 0 20px 0; }
3507
+ .csr-hr-tight { border: none; border-top: 1px solid #bbb; margin: 0 0 4px 0; }
3508
+ .csr-hr-none { border: none; border-top: 1px solid #bbb; margin: 0; }
3509
+ .csr-hero-normal { max-width: 100%; margin: 0 0 20px 0; }
3510
+ .csr-hero-tight { max-width: 100%; margin: 0 0 4px 0; }
3511
+ .csr-hero-none { max-width: 100%; margin: 0; }
3512
+ .csr-cta-box-normal { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0 0 20px 0; max-width: 220px; }
3513
+ .csr-cta-box-tight { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0 0 4px 0; max-width: 220px; }
3514
+ .csr-cta-box-none { display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0; max-width: 220px; }
3515
+ .csr-cta-cell-linked { text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; }
3516
+ .csr-cta-cell-code { padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; }
3517
+ .csr-cta-label { text-decoration: underline; }
3518
+ .csr-cta-anchor { display: block; padding: 16px 24px; color: #0047FF; text-decoration: none; }
3519
+ .csr-cta-code { color: #0047FF; }
3520
+ .csr-chat-rule-top { border: none; border-top: 1px solid #bbb; margin: 12px 0 24px 0; }
3521
+ .csr-chat-rule-bottom { border: none; border-top: 1px solid #bbb; margin: 24px 0 12px 0; }
3522
+ .csr-chat-row-24 { margin: 0 0 24px 0; }
3523
+ .csr-chat-row-16 { margin: 0 0 16px 0; }
3524
+ .csr-chat-attribution { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #666; text-align: right; padding-top: 6px; padding-right: 1ch; }
3525
+ .csr-chat-avatar-left { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; white-space: nowrap; }
3526
+ .csr-chat-avatar-left-hidden { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap; }
3527
+ .csr-chat-avatar-right { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; white-space: nowrap; }
3528
+ .csr-chat-avatar-right-hidden { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap; }
3529
+ .csr-chat-channel-left { width: 100%; text-align: left; }
3530
+ .csr-chat-channel-right { width: 100%; text-align: right; }
3531
+ .csr-chat-bubble-wrap { display: inline-block; max-width: 100%; vertical-align: bottom; }
3532
+ .csr-bubble-user { border-radius: 8px 8px 0 8px; padding: 10px 14px; text-align: right; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666; }
3533
+ .csr-bubble-assistant { border-radius: 8px 8px 8px 0; padding: 10px 14px; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666; }
3534
+ .csr-dots-over-cta { font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; font-weight: bold; color: #666; text-align: center; margin: 0 0 16px 0; }
3535
+ .csr-dots-cell { width: 100%; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; font-weight: bold; color: #666; }
3536
+ .csr-cta-stack { display: inline-block; text-align: left; }
3537
+ .cs-cta-hover:hover { background-color: #d1ffff; }</style></head>
3538
+ <body class="csr-body">
3539
+ <div class="csr-frame">
3540
+ <p class="csr-p-normal">Hi there,</p>
3541
+ <p class="csr-p-normal">Acme Corp uses Company Semantics.</p>
3542
+ <hr class="cs-faint csr-chat-rule-top">
3543
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="csr-chat-row-24">
3544
+ <tr>
3545
+ <td class="csr-chat-avatar-left-hidden">[c_S]</td>
3546
+ <td class="csr-chat-channel-right"><table cellpadding="0" cellspacing="0" border="0" class="csr-chat-bubble-wrap">
3547
+ <tr><td class="cs-bubble csr-bubble-user">We start planning next quarter on Monday — glad you are joining.</td></tr>
3548
+ </table></td>
3549
+ <td class="csr-chat-avatar-right">(•̀_ರ╮)</td>
3550
+ </tr>
3551
+ <tr>
3552
+ <td></td>
3553
+ <td class="cs-meta csr-chat-attribution">Alex Rivera</td>
3554
+ <td></td>
3555
+ </tr>
3556
+ </table>
3557
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="csr-chat-row-16">
3558
+ <tr>
3559
+ <td class="csr-chat-avatar-left">[c_S]</td>
3560
+ <td class="csr-chat-channel-left"><table cellpadding="0" cellspacing="0" border="0" class="csr-chat-bubble-wrap">
3561
+ <tr><td class="cs-bubble csr-bubble-assistant">Join to accept the invitation.</td></tr>
3562
+ </table></td>
3563
+ <td class="csr-chat-avatar-right-hidden">(•̀_ರ╮)</td>
3564
+ </tr>
3565
+ </table>
3566
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" class="csr-chat-row-24">
3567
+ <tr>
3568
+ <td class="csr-chat-avatar-left-hidden">[c_S]</td>
3569
+ <td class="csr-chat-channel-left"><div class="csr-cta-stack"><table cellpadding="0" cellspacing="0" border="0" class="cs-cta cs-cta-hover csr-cta-box-none">
3570
+ <tr><td class="csr-cta-cell-linked">
3571
+ <a href="https://app.companysemantics.ai/invite/ghi789" class="cs-link csr-cta-anchor">&gt;&gt; <span class="csr-cta-label">JOIN</span> &lt;&lt;</a>
3572
+ </td></tr>
3573
+ </table></div></td>
3574
+ <td class="csr-chat-avatar-right-hidden">(•̀_ರ╮)</td>
3575
+ </tr>
3576
+ </table>
3577
+ <hr class="cs-faint csr-chat-rule-bottom">
3578
+ <p class="csr-p-tight">From: Alex Rivera</p>
3579
+ <p class="csr-p-tight">Workspace: Acme Corp</p>
3580
+ <p class="csr-p-tight">Role: Member</p>
3581
+ <p class="csr-p-normal">Expires: Jun 13, 2026</p>
3582
+ <p class="csr-p-none">This invitation was sent via Company Semantics.<br>If you weren&#39;t expecting this, no action is required.</p>
3583
+ <p class="csr-p-none"><br><br><span class="cs-faint csr-eom">/* EOM */</span><br>ⓒ 2026 • Company Semantics<br><a href="https://companysemantics.ai" target="_blank" rel="noopener noreferrer" class="cs-link csr-signature-link">https://companysemantics.ai</a></p>
3584
+ </div>
3585
+ </body>
3586
+ </html>",
3587
+ "html": "<!DOCTYPE html>
3588
+ <html lang="en">
3589
+ <head><meta charset="UTF-8">
3590
+ <meta name="color-scheme" content="light dark">
3591
+ <meta name="supported-color-schemes" content="light dark">
3592
+ <style>.cs-cta-hover:hover { background-color: #d1ffff !important; }
3593
+ @media (prefers-color-scheme: dark) {
3594
+ body { background-color: #1a1a1a !important; color: #e8e8e8 !important; }
3595
+ .cs-faint { color: #666 !important; border-top-color: #666 !important; }
3596
+ .cs-meta { color: #8a8a8a !important; }
3597
+ .cs-cta { border-color: #00ffff !important; }
3598
+ .cs-cta-hover:hover { background-color: #154848 !important; }
3599
+ .cs-bubble { background: #154848 !important; color: #ffffff !important; }
3600
+ .cs-dots { color: #8a8a8a !important; }
3601
+ .cs-link { color: #00ffff !important; }
3602
+ .cs-destructive { color: #ff6b6b !important; }
3603
+ }</style>
3604
+ <!--[if mso]><style>.cs-cta td { padding: 16px 24px !important; }
3605
+ .cs-cta a { padding: 0 !important; }</style><![endif]--></head>
3606
+ <body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; background-color: #ffffff; margin: 0; padding: 0;">
3607
+ <div style="max-width: 520px; margin: 0 auto;">
3608
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Hi there,</p>
3609
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Acme Corp uses Company Semantics.</p>
3610
+ <hr class="cs-faint" style="border: none; border-top: 1px solid #bbb; margin: 12px 0 24px 0;">
3611
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 0 0 24px 0;">
3612
+ <tr>
3613
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">[c_S]</td>
3614
+ <td style="width: 100%; text-align: right;"><table cellpadding="0" cellspacing="0" border="0" style="display: inline-block; max-width: 100%; vertical-align: bottom;">
3615
+ <tr><td class="cs-bubble" style="border-radius: 8px 8px 0 8px; padding: 10px 14px; text-align: right; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666;">We start planning next quarter on Monday — glad you are joining.</td></tr>
3616
+ </table></td>
3617
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; white-space: nowrap;">(•̀_ರ╮)</td>
3618
+ </tr>
3619
+ <tr>
3620
+ <td></td>
3621
+ <td class="cs-meta" style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #666; text-align: right; padding-top: 6px; padding-right: 1ch;">Alex Rivera</td>
3622
+ <td></td>
3623
+ </tr>
3624
+ </table>
3625
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 0 0 16px 0;">
3626
+ <tr>
3627
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; white-space: nowrap;">[c_S]</td>
3628
+ <td style="width: 100%; text-align: left;"><table cellpadding="0" cellspacing="0" border="0" style="display: inline-block; max-width: 100%; vertical-align: bottom;">
3629
+ <tr><td class="cs-bubble" style="border-radius: 8px 8px 8px 0; padding: 10px 14px; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; color: #ffffff; background: #666;">Join to accept the invitation.</td></tr>
3630
+ </table></td>
3631
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">(•̀_ರ╮)</td>
3632
+ </tr>
3633
+ </table>
3634
+ <table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin: 0 0 24px 0;">
3635
+ <tr>
3636
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-right: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">[c_S]</td>
3637
+ <td style="width: 100%; text-align: left;"><div style="display: inline-block; text-align: left;"><table cellpadding="0" cellspacing="0" border="0" class="cs-cta cs-cta-hover" style="display: inline-block; border: 1px solid #0047FF; border-radius: 2px; margin: 0; max-width: 220px;">
3638
+ <tr><td style="text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px;">
3639
+ <a href="https://app.companysemantics.ai/invite/ghi789" class="cs-link" style="display: block; padding: 16px 24px; color: #0047FF; text-decoration: none;">&gt;&gt; <span style="text-decoration: underline;">JOIN</span> &lt;&lt;</a>
3640
+ </td></tr>
3641
+ </table></div></td>
3642
+ <td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; padding-left: 8px; vertical-align: bottom; visibility: hidden; white-space: nowrap;">(•̀_ರ╮)</td>
3643
+ </tr>
3644
+ </table>
3645
+ <hr class="cs-faint" style="border: none; border-top: 1px solid #bbb; margin: 24px 0 12px 0;">
3646
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0;">From: Alex Rivera</p>
3647
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0;">Workspace: Acme Corp</p>
3648
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 4px 0;">Role: Member</p>
3649
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0 0 20px 0;">Expires: Jun 13, 2026</p>
3650
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0;">This invitation was sent via Company Semantics.<br>If you weren&#39;t expecting this, no action is required.</p>
3651
+ <p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; margin: 0;"><br><br><span class="cs-faint" style="color: #bbb;">/* EOM */</span><br>ⓒ 2026 • Company Semantics<br><a href="https://companysemantics.ai" target="_blank" rel="noopener noreferrer" class="cs-link" style="color: #0047FF; text-decoration: none;">https://companysemantics.ai</a></p>
3652
+ </div>
3653
+ </body>
3654
+ </html>",
3655
+ "subject": "You've been invited to join Acme Corp on Company Semantics",
3656
+ "text": "Hi there,
3657
+
3658
+ Acme Corp uses Company Semantics.
3659
+
3660
+ ────────────────────────────────────────────────
3661
+
3662
+ ┌──────────────────────────────┐
3663
+ │ We start planning next │
3664
+ │ quarter on Monday — glad you │
3665
+ │ are joining. │ (•̀_ರ╮)
3666
+ └──────────────────────────────┘
3667
+ Alex Rivera
3668
+
3669
+ ┌──────────────────────────────┐
3670
+ │ Join to accept the │
3671
+ [c_S] │ invitation. │
3672
+ └──────────────────────────────┘
3673
+
3674
+ *----------------*
3675
+ | >> JOIN << |
3676
+ *----------------*
3677
+
3678
+ https://app.companysemantics.ai/invite/ghi789
3679
+ ────────────────────────────────────────────────
3680
+
3681
+ From: Alex Rivera
3682
+ Workspace: Acme Corp
3683
+ Role: Member
3684
+ Expires: Jun 13, 2026
3685
+
3686
+ This invitation was sent via Company Semantics.
3687
+ If you weren't expecting this, no action is
3688
+ required.
3689
+
3690
+
3484
3691
  /* EOM */
3485
3692
  ⓒ 2026 • Company Semantics
3486
3693
  https://companysemantics.ai