@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.
- package/package.json +4 -4
- package/src/__tests__/resource-keys.test.ts +30 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +33 -1
- package/src/chat/README.md +15 -4
- package/src/chat/__tests__/proactive-kind.test.ts +51 -0
- package/src/chat/index.ts +9 -0
- package/src/chat/proactive-kind.ts +51 -0
- package/src/chat/schemas.ts +92 -1
- package/src/chat/types.ts +19 -1
- package/src/index.ts +110 -0
- package/src/message-parts/README.md +5 -0
- package/src/message-parts/__tests__/suggested-replies.test.ts +52 -0
- package/src/message-parts/__tests__/wire.test.ts +48 -0
- package/src/message-parts/index.ts +8 -0
- package/src/message-parts/suggested-replies.ts +48 -0
- package/src/message-parts/types.ts +7 -1
- package/src/message-parts/wire.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/src/proactive/README.md +125 -0
- package/src/proactive/__tests__/README.md +56 -0
- package/src/proactive/__tests__/chat-templates.test.ts +167 -0
- package/src/proactive/__tests__/compile-fixtures.ts +110 -0
- package/src/proactive/__tests__/vocabulary.test.ts +279 -0
- package/src/proactive/classes.ts +125 -0
- package/src/proactive/composer.ts +104 -0
- package/src/proactive/facts.ts +87 -0
- package/src/proactive/index.ts +52 -0
- package/src/proactive/kinds.ts +127 -0
- package/src/proactive/plan.ts +79 -0
- package/src/proactive/registry.ts +71 -0
- package/src/proactive/surfaces.ts +59 -0
- package/src/proactive/templates/README.md +58 -0
- package/src/proactive/templates/index.ts +32 -0
- package/src/proactive/templates/morning-brief.ts +77 -0
- package/src/proactive/templates/org-became-shared.ts +54 -0
- package/src/resource-key-types.ts +9 -0
- package/src/resource-keys.ts +2 -0
- package/src/user-notifications/README.md +10 -0
- package/src/user-notifications/kinds.ts +28 -0
|
@@ -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 */
|
package/src/org/README.md
CHANGED
|
@@ -61,8 +61,14 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
61
61
|
- `CanonicalPersonSchema` — A person as reported by an external source, keyed by external identity.
|
|
62
62
|
- `CanonicalPosition` _(type)_
|
|
63
63
|
- `CanonicalPositionSchema` — A position (seat) as reported by an external source, placed in a unit by external reference…
|
|
64
|
+
- `CanonicalPositionStatus` _(type)_
|
|
65
|
+
- `CanonicalPositionStatusSchema` — Seat-existence as the SOURCE reported it.
|
|
64
66
|
- `CanonicalReportingEdge` _(type)_
|
|
65
67
|
- `CanonicalReportingEdgeSchema` — A reporting edge (a seat reports to a manager seat) as reported by an external source.
|
|
68
|
+
- `CanonicalUnresolvedManager` _(type)_
|
|
69
|
+
- `CanonicalUnresolvedManagerResolution` _(type)_
|
|
70
|
+
- `CanonicalUnresolvedManagerResolutionSchema` — Why a supervisory reference could not be resolved to a seat in this batch.
|
|
71
|
+
- `CanonicalUnresolvedManagerSchema` — A supervisory reference the source reported that we could NOT resolve to a seat in this batch.
|
|
66
72
|
- `CapabilitiesResponse` _(type)_
|
|
67
73
|
- `CapabilitiesResponseSchema` — The single validated capability surface the UI reads.
|
|
68
74
|
- `ChangeMemberRoleRequest` _(type)_ — Request payload for changing a member's role.
|
|
@@ -129,6 +135,10 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
129
135
|
- `ExecutionContext` _(type)_
|
|
130
136
|
- `ExecutionContextSchema` — ExecutionContext — the RUN in which an intent was produced or applied.
|
|
131
137
|
- `ExecutionScope` _(type)_ — Execution scope determines whose identity is used when executing actions. - 'self': Actions execute under the…
|
|
138
|
+
- `ExistingPlacementFact` _(type)_
|
|
139
|
+
- `ExistingPlacementFactSchema` — Where a person already sits, and on whose authority they sit there.
|
|
140
|
+
- `ExistingUnitFact` _(type)_
|
|
141
|
+
- `ExistingUnitFactSchema` — An org unit that already exists, with the license the engine has over it.
|
|
132
142
|
- `FACT_SOURCE_TIER_PRECEDENCE` — Truth hierarchy, highest precedence first.
|
|
133
143
|
- `FactMutation` _(type)_
|
|
134
144
|
- `FactMutationSchema` — A single atomic fact change the engine applies to the live org graph.
|
|
@@ -301,6 +311,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
301
311
|
- `OwnershipTransferResponseSchema`
|
|
302
312
|
- `OwnershipTransferStatus` _(type)_ — Status of pending ownership transfer for an organization.
|
|
303
313
|
- `PermissionAuditEntry` _(type)_ — Single entry in the permission change audit log.
|
|
314
|
+
- `PersonStructureOutcome` _(type)_
|
|
315
|
+
- `PersonStructureOutcomeSchema` — EXACTLY ONE outcome per person in the snapshot. "Zero or one placement" would accept a proposal that simply…
|
|
304
316
|
- `Phase3AuditAction` _(type)_ — Audit actions for Phase 3 workspace expansion features.
|
|
305
317
|
- `Phase4AuditAction` _(type)_ — Audit actions for Phase 4 enterprise identity features.
|
|
306
318
|
- `Position` _(type)_
|
|
@@ -311,6 +323,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
311
323
|
- `PositionSchema`
|
|
312
324
|
- `PositionStatus` _(type)_
|
|
313
325
|
- `PositionStatusSchema` — Lifecycle of a seat's EXISTENCE — distinct from a hiring pipeline.
|
|
326
|
+
- `ProposedOrgUnit` _(type)_
|
|
327
|
+
- `ProposedOrgUnitSchema` — A unit the proposal wants the org to have.
|
|
314
328
|
- `Provenance` _(type)_
|
|
315
329
|
- `ProvenanceSchema` — Provenance — WHO/WHAT generated an intent.
|
|
316
330
|
- `ProviderStatus` _(type)_ — Provider-level configuration lifecycle.
|
|
@@ -351,10 +365,33 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
351
365
|
- `SsoStepperStep` _(type)_ — Backend-authoritative stepper step.
|
|
352
366
|
- `StagedPlacement` _(type)_ — Staged member placement: the fuller org position an invitee is slotted into, applied as deferred grants when…
|
|
353
367
|
- `StagedPlacementSchema` — Staged member placement: the fuller org position an invitee is slotted into, applied as deferred grants when…
|
|
368
|
+
- `StructureAcceptedBy` _(type)_
|
|
369
|
+
- `StructureAcceptedBySchema` — WHO accepted the fact.
|
|
370
|
+
- `StructureAuthority` _(type)_
|
|
371
|
+
- `StructureAuthoritySchema` — The authority axis — the ONLY axis precedence runs on: `human_confirmed` `ai_inferred` `hris_source`.
|
|
372
|
+
- `StructureEvidence` _(type)_
|
|
373
|
+
- `StructureEvidenceSchema` — TYPED evidence.
|
|
374
|
+
- `StructureInferenceSnapshot` _(type)_
|
|
375
|
+
- `StructureInferenceSnapshotSchema` — The inference INPUT: a point-in-time view of the PERSISTED canonical graph.
|
|
376
|
+
- `StructurePersonFact` _(type)_
|
|
377
|
+
- `StructurePersonFactSchema` — A person as the inference engine sees them.
|
|
378
|
+
- `StructureProposal` _(type)_
|
|
379
|
+
- `StructureProposalOrigin` _(type)_
|
|
380
|
+
- `StructureProposalOriginSchema` — How a structural fact came to be PROPOSED.
|
|
381
|
+
- `StructureProposalSchema` — A proposed desired state for one org.
|
|
382
|
+
- `StructureProvenance` _(type)_
|
|
383
|
+
- `StructureProvenanceSchema` — The three axes together, as one accepted structural fact records them. - v1 — AI proposes, an admin clicks…
|
|
384
|
+
- `StructureReportingFact` _(type)_
|
|
385
|
+
- `StructureReportingFactSchema` — One reporting edge, at person granularity.
|
|
386
|
+
- `StructureReviewItem` _(type)_
|
|
387
|
+
- `StructureReviewItemSchema` — A question the engine is asking a human, with the competing signals attached.
|
|
388
|
+
- `StructureUnitAuthority` _(type)_
|
|
389
|
+
- `StructureUnitAuthoritySchema` — How much license the engine has over an EXISTING unit or placement.
|
|
354
390
|
- `SubmitInteractiveTaskResponse` _(type)_
|
|
355
391
|
- `SubmitInteractiveTaskResponseSchema` — Response for submitting a filled-in interactive chat task surface (e.g. the editable "change reporting…
|
|
356
392
|
- `SyncRunSummary` _(type)_
|
|
357
393
|
- `SyncRunSummarySchema`
|
|
394
|
+
- `TOPOLOGY_ONLY_EVIDENCE_KINDS` — Evidence kinds that alone are NOT sufficient to justify a unit boundary.
|
|
358
395
|
- `TRANSFER_RESPONSIBILITIES` — Responsibility checklist items shown on the acceptance page.
|
|
359
396
|
- `TestSsoInitiation` _(type)_ — Initiation payload for a test SSO login attempt.
|
|
360
397
|
- `TestSsoInitiationDto` _(type)_ — Initiation payload for a test SSO login attempt.
|
|
@@ -424,6 +461,7 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
424
461
|
- `WorkspaceSsoState` _(type)_ — Workspace SSO state derived from provider status + policy.
|
|
425
462
|
- `WorkspaceSurface` _(type)_ — Discriminator for workspace surface routing. - 'info': Read-only surface, any member, org.view capabilities -…
|
|
426
463
|
- `getViewScope` — Get the required scope for a view, or null if public.
|
|
464
|
+
- `mayOverrideAuthority` — True when `incoming` may overwrite a fact currently held at `existing`.
|
|
427
465
|
- `orderTreeNodes`
|
|
428
466
|
|
|
429
467
|
<!-- END GENERATED: readme-public-api -->
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CanonicalPositionSchema,
|
|
7
7
|
CanonicalOccupancySchema,
|
|
8
8
|
CanonicalReportingEdgeSchema,
|
|
9
|
+
CanonicalUnresolvedManagerSchema,
|
|
9
10
|
CanonicalFactsSchema,
|
|
10
11
|
} from "../canonical-facts.js";
|
|
11
12
|
|
|
@@ -105,12 +106,14 @@ describe("CanonicalPositionSchema", () => {
|
|
|
105
106
|
externalSourceSystem: "workday",
|
|
106
107
|
title: "Staff Engineer",
|
|
107
108
|
unitExternalSourceId: "dept-9",
|
|
109
|
+
status: "filled",
|
|
108
110
|
provenance,
|
|
109
111
|
};
|
|
110
112
|
|
|
111
113
|
it("parses a seat placed in a unit by external reference", () => {
|
|
112
114
|
const p = CanonicalPositionSchema.parse(base);
|
|
113
115
|
expect(p.unitExternalSourceId).toBe("dept-9");
|
|
116
|
+
expect(p.status).toBe("filled");
|
|
114
117
|
});
|
|
115
118
|
|
|
116
119
|
it("rejects an empty title or missing unit reference", () => {
|
|
@@ -120,6 +123,95 @@ describe("CanonicalPositionSchema", () => {
|
|
|
120
123
|
const { unitExternalSourceId: _u, ...without } = base;
|
|
121
124
|
expect(() => CanonicalPositionSchema.parse(without)).toThrow();
|
|
122
125
|
});
|
|
126
|
+
|
|
127
|
+
it("accepts every reported seat-existence value", () => {
|
|
128
|
+
for (const status of ["planned", "open", "filled"]) {
|
|
129
|
+
expect(CanonicalPositionSchema.parse({ ...base, status }).status).toBe(
|
|
130
|
+
status,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("requires status — an adapter that cannot say must say 'filled'", () => {
|
|
136
|
+
const { status: _s, ...without } = base;
|
|
137
|
+
expect(() => CanonicalPositionSchema.parse(without)).toThrow();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("rejects a status outside the reported-existence axis", () => {
|
|
141
|
+
// `closed` is an in-graph lifecycle outcome, not something a source reports.
|
|
142
|
+
expect(() =>
|
|
143
|
+
CanonicalPositionSchema.parse({ ...base, status: "closed" }),
|
|
144
|
+
).toThrow();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("CanonicalUnresolvedManagerSchema", () => {
|
|
149
|
+
const base = {
|
|
150
|
+
externalSourceId: "unresolved-mgr-42",
|
|
151
|
+
externalSourceSystem: "bamboohr",
|
|
152
|
+
externalManagerId: "42",
|
|
153
|
+
reportPositionExternalSourceIds: ["pos-7", "pos-8"],
|
|
154
|
+
provenance,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
it("parses a dangling supervisor reference with its report seats", () => {
|
|
158
|
+
const u = CanonicalUnresolvedManagerSchema.parse(base);
|
|
159
|
+
expect(u.externalManagerId).toBe("42");
|
|
160
|
+
expect(u.reportPositionExternalSourceIds).toEqual(["pos-7", "pos-8"]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("defaults resolution to 'unknown' — never to a vacancy", () => {
|
|
164
|
+
expect(CanonicalUnresolvedManagerSchema.parse(base).resolution).toBe(
|
|
165
|
+
"unknown",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("records a vacancy only when the source says so", () => {
|
|
170
|
+
expect(
|
|
171
|
+
CanonicalUnresolvedManagerSchema.parse({
|
|
172
|
+
...base,
|
|
173
|
+
resolution: "vacant_position",
|
|
174
|
+
}).resolution,
|
|
175
|
+
).toBe("vacant_position");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("accepts the remaining causes, which are not interchangeable", () => {
|
|
179
|
+
for (const resolution of ["inactive_employee", "not_visible"]) {
|
|
180
|
+
expect(
|
|
181
|
+
CanonicalUnresolvedManagerSchema.parse({ ...base, resolution })
|
|
182
|
+
.resolution,
|
|
183
|
+
).toBe(resolution);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("rejects an unknown resolution cause", () => {
|
|
188
|
+
expect(() =>
|
|
189
|
+
CanonicalUnresolvedManagerSchema.parse({
|
|
190
|
+
...base,
|
|
191
|
+
resolution: "terminated",
|
|
192
|
+
}),
|
|
193
|
+
).toThrow();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("requires the unresolvable manager id and a provenance envelope", () => {
|
|
197
|
+
expect(() =>
|
|
198
|
+
CanonicalUnresolvedManagerSchema.parse({
|
|
199
|
+
...base,
|
|
200
|
+
externalManagerId: "",
|
|
201
|
+
}),
|
|
202
|
+
).toThrow();
|
|
203
|
+
const { provenance: _omitted, ...without } = base;
|
|
204
|
+
expect(() => CanonicalUnresolvedManagerSchema.parse(without)).toThrow();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("accepts an empty report list (the reference dangles on its own)", () => {
|
|
208
|
+
expect(
|
|
209
|
+
CanonicalUnresolvedManagerSchema.parse({
|
|
210
|
+
...base,
|
|
211
|
+
reportPositionExternalSourceIds: [],
|
|
212
|
+
}).reportPositionExternalSourceIds,
|
|
213
|
+
).toEqual([]);
|
|
214
|
+
});
|
|
123
215
|
});
|
|
124
216
|
|
|
125
217
|
describe("CanonicalOccupancySchema", () => {
|
|
@@ -187,9 +279,11 @@ describe("CanonicalFactsSchema", () => {
|
|
|
187
279
|
positions: [],
|
|
188
280
|
occupancies: [],
|
|
189
281
|
reportingEdges: [],
|
|
282
|
+
unresolvedManagers: [],
|
|
190
283
|
});
|
|
191
284
|
expect(facts.people).toEqual([]);
|
|
192
285
|
expect(facts.reportingEdges).toEqual([]);
|
|
286
|
+
expect(facts.unresolvedManagers).toEqual([]);
|
|
193
287
|
});
|
|
194
288
|
|
|
195
289
|
it("parses a populated batch with one of each fact", () => {
|
|
@@ -216,6 +310,7 @@ describe("CanonicalFactsSchema", () => {
|
|
|
216
310
|
externalSourceSystem: "workday",
|
|
217
311
|
title: "Staff Engineer",
|
|
218
312
|
unitExternalSourceId: "dept-9",
|
|
313
|
+
status: "filled",
|
|
219
314
|
provenance,
|
|
220
315
|
},
|
|
221
316
|
],
|
|
@@ -238,9 +333,19 @@ describe("CanonicalFactsSchema", () => {
|
|
|
238
333
|
provenance,
|
|
239
334
|
},
|
|
240
335
|
],
|
|
336
|
+
unresolvedManagers: [
|
|
337
|
+
{
|
|
338
|
+
externalSourceId: "unresolved-mgr-42",
|
|
339
|
+
externalSourceSystem: "bamboohr",
|
|
340
|
+
externalManagerId: "42",
|
|
341
|
+
reportPositionExternalSourceIds: ["pos-7"],
|
|
342
|
+
provenance,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
241
345
|
});
|
|
242
346
|
expect(facts.people).toHaveLength(1);
|
|
243
347
|
expect(facts.positions[0].unitExternalSourceId).toBe("dept-9");
|
|
348
|
+
expect(facts.unresolvedManagers[0].resolution).toBe("unknown");
|
|
244
349
|
});
|
|
245
350
|
|
|
246
351
|
it("requires every array key to be present", () => {
|
|
@@ -249,6 +354,18 @@ describe("CanonicalFactsSchema", () => {
|
|
|
249
354
|
).toThrow();
|
|
250
355
|
});
|
|
251
356
|
|
|
357
|
+
it("requires the unresolvedManagers channel — silence is not an option", () => {
|
|
358
|
+
expect(() =>
|
|
359
|
+
CanonicalFactsSchema.parse({
|
|
360
|
+
people: [],
|
|
361
|
+
orgUnits: [],
|
|
362
|
+
positions: [],
|
|
363
|
+
occupancies: [],
|
|
364
|
+
reportingEdges: [],
|
|
365
|
+
}),
|
|
366
|
+
).toThrow();
|
|
367
|
+
});
|
|
368
|
+
|
|
252
369
|
it("rejects a member that fails its element schema", () => {
|
|
253
370
|
expect(() =>
|
|
254
371
|
CanonicalFactsSchema.parse({
|
|
@@ -264,6 +381,7 @@ describe("CanonicalFactsSchema", () => {
|
|
|
264
381
|
positions: [],
|
|
265
382
|
occupancies: [],
|
|
266
383
|
reportingEdges: [],
|
|
384
|
+
unresolvedManagers: [],
|
|
267
385
|
}),
|
|
268
386
|
).toThrow();
|
|
269
387
|
});
|