@company-semantics/contracts 51.3.0 → 52.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "51.3.0",
3
+ "version": "52.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import {
4
+ PeopleOrgChartSeatOccupantSchema,
5
+ PeopleOrgChartSeatSchema,
6
+ } from "../people-org-chart.js";
7
+
8
+ const PERSON_ID = "3f2504e0-4f89-41d3-9a0c-0305e82c3301";
9
+ const USER_ID = "9c5b94b1-35ad-49bb-b118-8e8fc24abf80";
10
+ const UNIT_ID = "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b";
11
+ const POSITION_ID = "11111111-1111-4111-8111-111111111111";
12
+
13
+ describe("PeopleOrgChartSeatOccupantSchema", () => {
14
+ const accountless = {
15
+ personId: PERSON_ID,
16
+ userId: null,
17
+ fullName: "Ada Lovelace",
18
+ avatarUrl: null,
19
+ primaryUnitId: null,
20
+ ownedUnits: [],
21
+ };
22
+
23
+ it("parses an accountless occupant keyed by personId", () => {
24
+ const occupant = PeopleOrgChartSeatOccupantSchema.parse(accountless);
25
+ expect(occupant.personId).toBe(PERSON_ID);
26
+ expect(occupant.userId).toBeNull();
27
+ });
28
+
29
+ it("parses an account-linked occupant carrying a uuid userId", () => {
30
+ const occupant = PeopleOrgChartSeatOccupantSchema.parse({
31
+ ...accountless,
32
+ userId: USER_ID,
33
+ });
34
+ expect(occupant.userId).toBe(USER_ID);
35
+ });
36
+
37
+ it("requires personId", () => {
38
+ const { personId: _omitted, ...withoutPersonId } = accountless;
39
+ expect(() =>
40
+ PeopleOrgChartSeatOccupantSchema.parse(withoutPersonId),
41
+ ).toThrow();
42
+ });
43
+
44
+ // userId is .nullable() WITHOUT .optional(): the server always emits the key,
45
+ // so an absent userId must be unrepresentable rather than silently null.
46
+ it("rejects an absent userId — nullable is not optional", () => {
47
+ const { userId: _omitted, ...withoutUserId } = accountless;
48
+ const result = PeopleOrgChartSeatOccupantSchema.safeParse(withoutUserId);
49
+ expect(result.success).toBe(false);
50
+ });
51
+ });
52
+
53
+ describe("PeopleOrgChartSeatSchema", () => {
54
+ it("admits a filled seat whose only occupant is accountless", () => {
55
+ const seat = PeopleOrgChartSeatSchema.parse({
56
+ id: POSITION_ID,
57
+ unitId: UNIT_ID,
58
+ title: "Analyst",
59
+ status: "filled",
60
+ hiring: false,
61
+ occupants: [
62
+ {
63
+ personId: PERSON_ID,
64
+ userId: null,
65
+ fullName: "Ada Lovelace",
66
+ avatarUrl: null,
67
+ primaryUnitId: null,
68
+ ownedUnits: [],
69
+ },
70
+ ],
71
+ });
72
+ expect(seat.occupants).toHaveLength(1);
73
+ expect(seat.occupants[0]?.userId).toBeNull();
74
+ });
75
+ });
@@ -88,11 +88,30 @@ export type PeopleOrgChartSeatStatus = z.infer<
88
88
  typeof PeopleOrgChartSeatStatusSchema
89
89
  >;
90
90
 
91
- // Person-scoped attributes of a seat's holder — keyed by `userId`, they travel
92
- // with the person across seats (ADR-BE-329: a vacant seat grants none of these
93
- // by construction). Mirrors the legacy `PeopleOrgChartNode` person fields.
91
+ // Person-scoped attributes of a seat's holder — they travel with the person
92
+ // across seats (ADR-BE-329: a vacant seat grants none of these by
93
+ // construction).
94
+ //
95
+ // KEYED BY personId. The org chart is a graph of POSITIONS with PEOPLE
96
+ // projected onto them; the account link is an attribute of the person, never
97
+ // their identity. `userId` is the 0..1 account link and is null for the many
98
+ // people who never sign in (an HRIS import is the normal case, not the edge).
99
+ //
100
+ // For an ACCOUNTLESS occupant each of the following is a FACT, not a fallback:
101
+ // avatarUrl always null -- `persons` carries no avatar column
102
+ // primaryUnitId always null -- `home_assignments` is user-keyed, so there is
103
+ // no representable home; null means "no home
104
+ // row", NOT "home unknown, substitute the seat"
105
+ // ownedUnits always [] -- the projection of "no grant row exists".
106
+ // Authority is account-keyed; an accountless
107
+ // person holds none. This is the authority
108
+ // boundary made visible.
94
109
  export const PeopleOrgChartSeatOccupantSchema = z.object({
95
- userId: z.string().uuid(),
110
+ personId: z.string().uuid(),
111
+ // .nullable() WITHOUT .optional(): this is a server-generated response, so
112
+ // `undefined` must be unrepresentable. PersonSchema.userId is
113
+ // .nullable().optional() because it is an input-shaped vocabulary.
114
+ userId: z.string().uuid().nullable(),
96
115
  fullName: z.string(),
97
116
  avatarUrl: z.string().nullable(),
98
117
  // Stable HOME unit (home_assignments), not the seat unit — mirrors the legacy
@@ -123,8 +142,8 @@ export const PeopleOrgChartSeatSchema = z.object({
123
142
  // fulfillment row ⇒ false.
124
143
  hiring: z.boolean(),
125
144
  // The seat's current holders. [] ⇒ vacant (the old "open role"); >= 1 ⇒
126
- // occupied (the old "member"). Only account-linked, active/interim occupants
127
- // appear, mirroring the legacy node-keying.
145
+ // occupied (the old "member"). Every canonical occupant appears, account or
146
+ // not — occupancy, not account linkage, is what admits a person here.
128
147
  occupants: z.array(PeopleOrgChartSeatOccupantSchema),
129
148
  });
130
149