@company-semantics/contracts 57.0.0 → 58.1.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 +4 -4
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +12 -0
- package/src/index.ts +52 -0
- package/src/integrations/README.md +4 -0
- package/src/integrations/schemas.ts +26 -0
- package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +207 -0
- package/src/notifications/__tests__/fixtures.ts +9 -0
- package/src/notifications/__tests__/org-invite.test.ts +75 -0
- package/src/notifications/__tests__/render-snapshot.test.ts +8 -0
- package/src/notifications/kinds/org-invite.ts +27 -12
- package/src/notifications/payloads.ts +7 -0
- package/src/org/README.md +38 -0
- package/src/org/__tests__/canonical-facts.test.ts +118 -0
- package/src/org/__tests__/structure-inference.test.ts +392 -0
- package/src/org/__tests__/structure-provenance.test.ts +187 -0
- package/src/org/canonical-facts.ts +94 -1
- package/src/org/index.ts +54 -0
- package/src/org/schemas.ts +23 -0
- package/src/org/structure-inference.ts +521 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "58.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"@types/node": "^22.20.1",
|
|
143
143
|
"amphtml-validator": "^1.0.38",
|
|
144
144
|
"culori": "^4.0.2",
|
|
145
|
-
"eslint": "^10.
|
|
145
|
+
"eslint": "^10.9.0",
|
|
146
146
|
"husky": "^9.1.7",
|
|
147
147
|
"lint-staged": "^17.3.0",
|
|
148
148
|
"markdownlint-cli2": "^0.23.2",
|
|
@@ -151,8 +151,8 @@
|
|
|
151
151
|
"tsx": "^4.23.12",
|
|
152
152
|
"typescript": "^5.8.3",
|
|
153
153
|
"typescript-eslint": "^8.67.0",
|
|
154
|
-
"vite": "^8.2.
|
|
155
|
-
"vitest": "^4.1.
|
|
154
|
+
"vite": "^8.2.2",
|
|
155
|
+
"vitest": "^4.1.11",
|
|
156
156
|
"yaml": "^2.9.0"
|
|
157
157
|
},
|
|
158
158
|
"pnpm": {
|
|
@@ -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 = '49b741e4372d' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '49b741e4372d56fb1a87312fe62f6e86ffd4987de590d03414ee833a4b6586b0' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -4834,6 +4834,10 @@ export interface components {
|
|
|
4834
4834
|
};
|
|
4835
4835
|
SendInviteBatchRequest: {
|
|
4836
4836
|
expectedRevision: number;
|
|
4837
|
+
message?: string;
|
|
4838
|
+
roleByPersonId?: {
|
|
4839
|
+
[key: string]: "admin" | "member";
|
|
4840
|
+
};
|
|
4837
4841
|
};
|
|
4838
4842
|
InviteValidationResult: {
|
|
4839
4843
|
valid: boolean;
|
|
@@ -5989,6 +5993,14 @@ export interface components {
|
|
|
5989
5993
|
/** @enum {string} */
|
|
5990
5994
|
relationshipType: "solid" | "dotted";
|
|
5991
5995
|
}[];
|
|
5996
|
+
unplaced: {
|
|
5997
|
+
personId: string;
|
|
5998
|
+
fullName: string;
|
|
5999
|
+
/** @enum {string} */
|
|
6000
|
+
state: "needs_review" | "excluded";
|
|
6001
|
+
/** @enum {string} */
|
|
6002
|
+
reason: "insufficient_evidence" | "conflicting_evidence" | "ambiguous_boundary" | "service_account" | "not_workforce" | "inactive";
|
|
6003
|
+
}[];
|
|
5992
6004
|
};
|
|
5993
6005
|
/** @description Polling snapshot of a generic ingestion operation. */
|
|
5994
6006
|
IngestionOperationPollResponse: {
|
package/src/index.ts
CHANGED
|
@@ -798,20 +798,72 @@ export type {
|
|
|
798
798
|
export {
|
|
799
799
|
CanonicalPersonSchema,
|
|
800
800
|
CanonicalOrgUnitSchema,
|
|
801
|
+
CanonicalPositionStatusSchema,
|
|
801
802
|
CanonicalPositionSchema,
|
|
802
803
|
CanonicalOccupancySchema,
|
|
803
804
|
CanonicalReportingEdgeSchema,
|
|
805
|
+
CanonicalUnresolvedManagerResolutionSchema,
|
|
806
|
+
CanonicalUnresolvedManagerSchema,
|
|
804
807
|
CanonicalFactsSchema,
|
|
805
808
|
} from "./org/index";
|
|
806
809
|
export type {
|
|
807
810
|
CanonicalPerson,
|
|
808
811
|
CanonicalOrgUnit,
|
|
812
|
+
CanonicalPositionStatus,
|
|
809
813
|
CanonicalPosition,
|
|
810
814
|
CanonicalOccupancy,
|
|
811
815
|
CanonicalReportingEdge,
|
|
816
|
+
CanonicalUnresolvedManagerResolution,
|
|
817
|
+
CanonicalUnresolvedManager,
|
|
812
818
|
CanonicalFacts,
|
|
813
819
|
} from "./org/index";
|
|
814
820
|
|
|
821
|
+
// Org-structure inference: a StructureInferenceSnapshot of the persisted graph
|
|
822
|
+
// goes in, a StructureProposal — a proposed desired state, never database
|
|
823
|
+
// commands — comes out. Typed evidence, targetUnitId for durable identity, and
|
|
824
|
+
// exactly one PersonStructureOutcome per person in the snapshot. (PRD-00955)
|
|
825
|
+
//
|
|
826
|
+
// Acceptance attaches StructureProvenance — proposalOrigin, acceptedBy and
|
|
827
|
+
// authority as three ORTHOGONAL axes, so accepting an AI proposal never rewrites
|
|
828
|
+
// its origin as manual. mayOverrideAuthority is the single definition of
|
|
829
|
+
// precedence, and it consults `authority` only. (PRD-00956)
|
|
830
|
+
export {
|
|
831
|
+
StructureEvidenceSchema,
|
|
832
|
+
TOPOLOGY_ONLY_EVIDENCE_KINDS,
|
|
833
|
+
StructureUnitAuthoritySchema,
|
|
834
|
+
StructureProposalOriginSchema,
|
|
835
|
+
StructureAcceptedBySchema,
|
|
836
|
+
StructureAuthoritySchema,
|
|
837
|
+
StructureProvenanceSchema,
|
|
838
|
+
mayOverrideAuthority,
|
|
839
|
+
ProposedOrgUnitSchema,
|
|
840
|
+
PersonStructureOutcomeSchema,
|
|
841
|
+
StructureReviewItemSchema,
|
|
842
|
+
StructureProposalSchema,
|
|
843
|
+
StructurePersonFactSchema,
|
|
844
|
+
StructureReportingFactSchema,
|
|
845
|
+
ExistingUnitFactSchema,
|
|
846
|
+
ExistingPlacementFactSchema,
|
|
847
|
+
StructureInferenceSnapshotSchema,
|
|
848
|
+
} from "./org/index";
|
|
849
|
+
export type {
|
|
850
|
+
StructureEvidence,
|
|
851
|
+
StructureUnitAuthority,
|
|
852
|
+
StructureProposalOrigin,
|
|
853
|
+
StructureAcceptedBy,
|
|
854
|
+
StructureAuthority,
|
|
855
|
+
StructureProvenance,
|
|
856
|
+
ProposedOrgUnit,
|
|
857
|
+
PersonStructureOutcome,
|
|
858
|
+
StructureReviewItem,
|
|
859
|
+
StructureProposal,
|
|
860
|
+
StructurePersonFact,
|
|
861
|
+
StructureReportingFact,
|
|
862
|
+
ExistingUnitFact,
|
|
863
|
+
ExistingPlacementFact,
|
|
864
|
+
StructureInferenceSnapshot,
|
|
865
|
+
} from "./org/index";
|
|
866
|
+
|
|
815
867
|
// Authority & Delegation vocabulary
|
|
816
868
|
export {
|
|
817
869
|
AuthoritySourceSchema,
|
|
@@ -16,6 +16,10 @@ alongside `Meetings`, `Comms`, and `Docs`.
|
|
|
16
16
|
|
|
17
17
|
- `HrConnectionStatusSchema` is a read projection: count and sync fields are
|
|
18
18
|
`null` until the first successful sync (a connected, never-synced integration).
|
|
19
|
+
- `revokedReason` is non-null only when `credentialState` is `'unusable'`, and
|
|
20
|
+
its enum mirrors the backend's `ConnectionRevokedReason` exactly — the app
|
|
21
|
+
`parse`s this projection, so a value missing from the enum throws rather than
|
|
22
|
+
degrades (ADR-CONTRACTS-138).
|
|
19
23
|
- `HrConnectInputSchema` carries only the customer's provider `subdomain`; no
|
|
20
24
|
secrets or provider credentials live in contracts.
|
|
21
25
|
- Pure vocabulary — `zod` is the only runtime import (vocabulary-guard).
|
|
@@ -83,6 +83,32 @@ export const HrConnectionStatusSchema = z
|
|
|
83
83
|
description:
|
|
84
84
|
"Health of the most recent sync; null when no sync has run yet.",
|
|
85
85
|
}),
|
|
86
|
+
revokedReason: z
|
|
87
|
+
.enum([
|
|
88
|
+
"token_refresh_failed",
|
|
89
|
+
"missing_refresh_token",
|
|
90
|
+
"member_removed",
|
|
91
|
+
"app_uninstalled",
|
|
92
|
+
"tokens_revoked",
|
|
93
|
+
])
|
|
94
|
+
.nullable()
|
|
95
|
+
.meta({
|
|
96
|
+
description:
|
|
97
|
+
"Why the credential died, when `credentialState` is 'unusable'. " +
|
|
98
|
+
"'token_refresh_failed' — the provider rejected the refresh grant, " +
|
|
99
|
+
"which is a sign-in that simply aged out and is fixed by " +
|
|
100
|
+
"reconnecting. 'missing_refresh_token' — there is nothing left to " +
|
|
101
|
+
"refresh with, so reconnecting is the ONLY repair. " +
|
|
102
|
+
"'member_removed' — the person whose account backed this " +
|
|
103
|
+
"connection left the org, so someone else must reconnect it. " +
|
|
104
|
+
"'app_uninstalled' / 'tokens_revoked' — the grant was withdrawn on " +
|
|
105
|
+
"the provider's side, and reconnecting will fail again until it is " +
|
|
106
|
+
"restored there. The distinction matters because the four cases " +
|
|
107
|
+
"ask different things of the reader: three are 'click Reconnect' " +
|
|
108
|
+
"and one is 'go fix it in the provider first'. Null whenever " +
|
|
109
|
+
"`credentialState` is not 'unusable', and null on a connection " +
|
|
110
|
+
"revoked before this field existed.",
|
|
111
|
+
}),
|
|
86
112
|
})
|
|
87
113
|
.meta({
|
|
88
114
|
description: "Connection-status projection for an HR integration.",
|
|
@@ -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">>> <span class="csr-cta-label">JOIN</span> <<</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'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;">>> <span style="text-decoration: underline;">JOIN</span> <<</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'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
|
|
@@ -86,6 +86,15 @@ add("org.invite", "Member", {
|
|
|
86
86
|
expiresAt: "2026-06-13T00:00:00.000Z",
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
add("org.invite", "Member + note", {
|
|
90
|
+
inviterName: "Alex Rivera",
|
|
91
|
+
orgName: "Acme Corp",
|
|
92
|
+
role: "member",
|
|
93
|
+
acceptUrl: `${APP}/invite/ghi789`,
|
|
94
|
+
expiresAt: "2026-06-13T00:00:00.000Z",
|
|
95
|
+
message: "We start planning next quarter on Monday — glad you are joining.",
|
|
96
|
+
});
|
|
97
|
+
|
|
89
98
|
add("org.unit_owner_granted", "Unit owner + message", {
|
|
90
99
|
granterName: "Jordan Lee",
|
|
91
100
|
recipientName: "Sam Chen",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The invitation's optional note.
|
|
3
|
+
*
|
|
4
|
+
* An invite is often the first thing someone ever receives from a workspace, so
|
|
5
|
+
* the one line of human context on it does real work — and it has to arrive
|
|
6
|
+
* ATTRIBUTED. A note rendered as product copy reads as boilerplate, which is
|
|
7
|
+
* the one thing it is not.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { createRenderContext } from "../context";
|
|
13
|
+
import type { ChatUnitItem, NotificationContent } from "../content";
|
|
14
|
+
import { orgInviteDefinition } from "../kinds/org-invite";
|
|
15
|
+
import type { NotificationPayloads } from "../payloads";
|
|
16
|
+
|
|
17
|
+
const CONTEXT = createRenderContext();
|
|
18
|
+
|
|
19
|
+
const payload = (
|
|
20
|
+
over: Partial<NotificationPayloads["org.invite"]> = {},
|
|
21
|
+
): NotificationPayloads["org.invite"] => ({
|
|
22
|
+
inviterName: "Alex Rivera",
|
|
23
|
+
orgName: "Acme Corp",
|
|
24
|
+
role: "member",
|
|
25
|
+
acceptUrl: "https://app.companysemantics.ai/invite/abc123",
|
|
26
|
+
expiresAt: "2026-06-13T00:00:00.000Z",
|
|
27
|
+
...over,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function chatItems(content: NotificationContent): ChatUnitItem[] {
|
|
31
|
+
const elements = content.sections.flatMap((s) => s.elements);
|
|
32
|
+
const unit = elements.find((e) => e.type === "chatUnit");
|
|
33
|
+
if (!unit || unit.type !== "chatUnit") throw new Error("no chat unit");
|
|
34
|
+
return [...unit.items];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("org.invite", () => {
|
|
38
|
+
it("carries the note as the inviter's own words, above the join prompt", () => {
|
|
39
|
+
const items = chatItems(
|
|
40
|
+
orgInviteDefinition.compose(
|
|
41
|
+
payload({ message: "Glad you are joining." }),
|
|
42
|
+
CONTEXT,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
expect(items[0]).toEqual({
|
|
46
|
+
type: "message",
|
|
47
|
+
role: "user",
|
|
48
|
+
text: "Glad you are joining.",
|
|
49
|
+
from: "Alex Rivera",
|
|
50
|
+
});
|
|
51
|
+
// The prompt survives the note: the message is WHY someone was invited, the
|
|
52
|
+
// prompt is what to do about it.
|
|
53
|
+
expect(items[1]).toMatchObject({
|
|
54
|
+
role: "assistant",
|
|
55
|
+
text: "Join to accept the invitation.",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders no message block at all without one", () => {
|
|
60
|
+
const items = chatItems(orgInviteDefinition.compose(payload(), CONTEXT));
|
|
61
|
+
expect(items.some((i) => i.type === "message" && i.role === "user")).toBe(
|
|
62
|
+
false,
|
|
63
|
+
);
|
|
64
|
+
expect(items[0]).toMatchObject({ role: "assistant" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("keeps the call to action last either way", () => {
|
|
68
|
+
for (const message of [undefined, "Welcome."]) {
|
|
69
|
+
const items = chatItems(
|
|
70
|
+
orgInviteDefinition.compose(payload({ message }), CONTEXT),
|
|
71
|
+
);
|
|
72
|
+
expect(items[items.length - 1]).toMatchObject({ type: "callToAction" });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -92,6 +92,14 @@ add("org.invite", "Member", {
|
|
|
92
92
|
acceptUrl: `${APP}/invite/def456`,
|
|
93
93
|
expiresAt: "2026-06-13T00:00:00.000Z",
|
|
94
94
|
});
|
|
95
|
+
add("org.invite", "Member + note", {
|
|
96
|
+
inviterName: "Alex Rivera",
|
|
97
|
+
orgName: "Acme Corp",
|
|
98
|
+
role: "member",
|
|
99
|
+
acceptUrl: `${APP}/invite/ghi789`,
|
|
100
|
+
expiresAt: "2026-06-13T00:00:00.000Z",
|
|
101
|
+
message: "We start planning next quarter on Monday — glad you are joining.",
|
|
102
|
+
});
|
|
95
103
|
|
|
96
104
|
add("org.unit_owner_granted", "Unit owner + message", {
|
|
97
105
|
granterName: "Jordan Lee",
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* nothing left to catch. See ADR-CONTRACTS-085.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { ChatUnitItem } from "../content";
|
|
10
11
|
import type { NotificationDefinition } from "../definition";
|
|
11
12
|
|
|
12
13
|
import { formatExpiry, NOTICE, titleCase } from "../text";
|
|
@@ -14,7 +15,31 @@ import { formatExpiry, NOTICE, titleCase } from "../text";
|
|
|
14
15
|
export const orgInviteDefinition: NotificationDefinition<"org.invite"> = {
|
|
15
16
|
kind: "org.invite",
|
|
16
17
|
compose: (payload, context) => {
|
|
17
|
-
const { inviterName, orgName, role, acceptUrl, expiresAt } =
|
|
18
|
+
const { inviterName, orgName, role, acceptUrl, expiresAt, message } =
|
|
19
|
+
payload;
|
|
20
|
+
|
|
21
|
+
// The inviter's own words come FIRST and keep their name on them, above the
|
|
22
|
+
// prompt to join — a note attributed to nobody reads as product copy, which
|
|
23
|
+
// is the one thing it is not. The join prompt stays either way: the message
|
|
24
|
+
// is why this person was invited, the prompt is what to do about it.
|
|
25
|
+
const items: ChatUnitItem[] = [
|
|
26
|
+
...(message
|
|
27
|
+
? [
|
|
28
|
+
{
|
|
29
|
+
type: "message" as const,
|
|
30
|
+
role: "user" as const,
|
|
31
|
+
text: message,
|
|
32
|
+
from: inviterName,
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
: []),
|
|
36
|
+
{
|
|
37
|
+
type: "message",
|
|
38
|
+
role: "assistant",
|
|
39
|
+
text: "Join to accept the invitation.",
|
|
40
|
+
},
|
|
41
|
+
{ type: "callToAction", label: "JOIN", href: acceptUrl },
|
|
42
|
+
];
|
|
18
43
|
|
|
19
44
|
return {
|
|
20
45
|
metadata: {
|
|
@@ -26,17 +51,7 @@ export const orgInviteDefinition: NotificationDefinition<"org.invite"> = {
|
|
|
26
51
|
elements: [
|
|
27
52
|
{ type: "greeting" },
|
|
28
53
|
{ type: "body", text: `${orgName} uses Company Semantics.` },
|
|
29
|
-
{
|
|
30
|
-
type: "chatUnit",
|
|
31
|
-
items: [
|
|
32
|
-
{
|
|
33
|
-
type: "message",
|
|
34
|
-
role: "assistant",
|
|
35
|
-
text: "Join to accept the invitation.",
|
|
36
|
-
},
|
|
37
|
-
{ type: "callToAction", label: "JOIN", href: acceptUrl },
|
|
38
|
-
],
|
|
39
|
-
},
|
|
54
|
+
{ type: "chatUnit", items },
|
|
40
55
|
{
|
|
41
56
|
type: "keyValueTable",
|
|
42
57
|
rows: [
|
|
@@ -51,6 +51,13 @@ export interface NotificationPayloads {
|
|
|
51
51
|
acceptUrl: string;
|
|
52
52
|
/** ISO timestamp when the invitation expires. Rendered as "Expires: Jun 13, 2026". */
|
|
53
53
|
expiresAt: string;
|
|
54
|
+
/**
|
|
55
|
+
* Optional note from the inviter, shown above the join prompt and attributed
|
|
56
|
+
* to them — the same shape `share.granted` and `org.unit_owner_granted` give
|
|
57
|
+
* a granter's message. Absent means no message block at all, never an empty
|
|
58
|
+
* one.
|
|
59
|
+
*/
|
|
60
|
+
message?: string;
|
|
54
61
|
};
|
|
55
62
|
"org.unit_owner_granted": {
|
|
56
63
|
/** Display name of the person who granted access */
|