@company-semantics/contracts 13.15.0 → 13.17.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": "13.15.0",
3
+ "version": "13.17.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,6 +31,34 @@ describe("resource-keys: orgUnitOwners (org-scoped)", () => {
31
31
  });
32
32
  });
33
33
 
34
+ describe("resource-keys: orgUnitOpenRoles (per-unit identity)", () => {
35
+ const UNIT_ID = "33333333-3333-4333-8333-333333333333";
36
+ const key: ResourceKey = {
37
+ type: "orgUnitOpenRoles",
38
+ orgId: ORG_ID,
39
+ unitId: UNIT_ID,
40
+ };
41
+
42
+ it("toQueryKey produces [type, orgId, unitId]", () => {
43
+ const qk = toQueryKey(key);
44
+ expect(qk).toEqual(["orgUnitOpenRoles", ORG_ID, UNIT_ID]);
45
+ expect(qk).toHaveLength(3);
46
+ });
47
+
48
+ it("roundtrips through fromQueryKey", () => {
49
+ const parsed = fromQueryKey(toQueryKey(key));
50
+ expect(parsed).toEqual(key);
51
+ });
52
+
53
+ it("matchesResourceKey is unitId-scoped: rejects a different unit", () => {
54
+ const otherUnit = "44444444-4444-4444-8444-444444444444";
55
+ expect(matchesResourceKey(toQueryKey(key), key)).toBe(true);
56
+ expect(
57
+ matchesResourceKey(["orgUnitOpenRoles", ORG_ID, otherUnit], key),
58
+ ).toBe(false);
59
+ });
60
+ });
61
+
34
62
  describe("resource-keys: system-scoped internalAdmin* types", () => {
35
63
  const systemTypes = [
36
64
  "internalAdminAiProviders",
@@ -1972,6 +1972,24 @@ export interface paths {
1972
1972
  patch?: never;
1973
1973
  trace?: never;
1974
1974
  };
1975
+ "/api/org-units/{unitId}/head": {
1976
+ parameters: {
1977
+ query?: never;
1978
+ header?: never;
1979
+ path?: never;
1980
+ cookie?: never;
1981
+ };
1982
+ get?: never;
1983
+ /** Pin an explicit head (anchor person) for an org unit */
1984
+ put: operations["setOrgUnitHead"];
1985
+ post?: never;
1986
+ /** Revert an org unit head to the system-inferred default */
1987
+ delete: operations["revertOrgUnitHead"];
1988
+ options?: never;
1989
+ head?: never;
1990
+ patch?: never;
1991
+ trace?: never;
1992
+ };
1975
1993
  "/api/org-units/{unitId}/my-authority": {
1976
1994
  parameters: {
1977
1995
  query?: never;
@@ -4287,6 +4305,8 @@ export interface components {
4287
4305
  count: number;
4288
4306
  userIds: string[];
4289
4307
  } | null;
4308
+ headUserId: string | null;
4309
+ headOrigin: ("user" | "inferred") | null;
4290
4310
  }[];
4291
4311
  };
4292
4312
  OrgUnitDescendantsResponse: {
@@ -4435,6 +4455,8 @@ export interface components {
4435
4455
  count: number;
4436
4456
  userIds: string[];
4437
4457
  } | null;
4458
+ headUserId: string | null;
4459
+ headOrigin: ("user" | "inferred") | null;
4438
4460
  }[];
4439
4461
  levelConfig: {
4440
4462
  /** Format: uuid */
@@ -4579,6 +4601,12 @@ export interface components {
4579
4601
  revokedAt: string | null;
4580
4602
  reason: string | null;
4581
4603
  };
4604
+ UnitHeadResponse: {
4605
+ /** Format: uuid */
4606
+ unitId: string;
4607
+ headUserId: string | null;
4608
+ origin: ("user" | "inferred") | null;
4609
+ };
4582
4610
  OrgUnitMyAuthorityResponse: {
4583
4611
  /** Format: uuid */
4584
4612
  unitId: string;
@@ -4693,6 +4721,14 @@ export interface components {
4693
4721
  unitId: string;
4694
4722
  title: string | null;
4695
4723
  }[];
4724
+ unitHeads: {
4725
+ /** Format: uuid */
4726
+ unitId: string;
4727
+ /** Format: uuid */
4728
+ headUserId: string;
4729
+ /** @enum {string} */
4730
+ origin: "user" | "inferred";
4731
+ }[];
4696
4732
  };
4697
4733
  /** @description Polling snapshot of a generic ingestion operation. */
4698
4734
  IngestionOperationPollResponse: {
@@ -8444,6 +8480,57 @@ export interface operations {
8444
8480
  };
8445
8481
  };
8446
8482
  };
8483
+ setOrgUnitHead: {
8484
+ parameters: {
8485
+ query?: never;
8486
+ header?: never;
8487
+ path: {
8488
+ unitId: string;
8489
+ };
8490
+ cookie?: never;
8491
+ };
8492
+ requestBody: {
8493
+ content: {
8494
+ "application/json": {
8495
+ /** Format: uuid */
8496
+ userId: string;
8497
+ };
8498
+ };
8499
+ };
8500
+ responses: {
8501
+ /** @description The pinned unit head */
8502
+ 200: {
8503
+ headers: {
8504
+ [name: string]: unknown;
8505
+ };
8506
+ content: {
8507
+ "application/json": components["schemas"]["UnitHeadResponse"];
8508
+ };
8509
+ };
8510
+ };
8511
+ };
8512
+ revertOrgUnitHead: {
8513
+ parameters: {
8514
+ query?: never;
8515
+ header?: never;
8516
+ path: {
8517
+ unitId: string;
8518
+ };
8519
+ cookie?: never;
8520
+ };
8521
+ requestBody?: never;
8522
+ responses: {
8523
+ /** @description The re-inferred unit head (or null when none resolves) */
8524
+ 200: {
8525
+ headers: {
8526
+ [name: string]: unknown;
8527
+ };
8528
+ content: {
8529
+ "application/json": components["schemas"]["UnitHeadResponse"];
8530
+ };
8531
+ };
8532
+ };
8533
+ };
8447
8534
  getMyOrgUnitAuthority: {
8448
8535
  parameters: {
8449
8536
  query?: never;
@@ -67,6 +67,8 @@ export {
67
67
  PeopleOrgChartNodeSchema,
68
68
  PeopleOrgChartEdgeSchema,
69
69
  PeopleOrgChartOpenRoleSchema,
70
+ UnitHeadOriginSchema,
71
+ PeopleOrgChartUnitHeadSchema,
70
72
  PeopleOrgChartResponseSchema,
71
73
  } from "./people-org-chart";
72
74
  export type {
@@ -74,5 +76,7 @@ export type {
74
76
  PeopleOrgChartNode,
75
77
  PeopleOrgChartEdge,
76
78
  PeopleOrgChartOpenRole,
79
+ UnitHeadOrigin,
80
+ PeopleOrgChartUnitHead,
77
81
  PeopleOrgChartResponse,
78
82
  } from "./people-org-chart";
@@ -66,6 +66,29 @@ export type PeopleOrgChartOpenRole = z.infer<
66
66
  typeof PeopleOrgChartOpenRoleSchema
67
67
  >;
68
68
 
69
+ // ---------------------------------------------------------------------------
70
+ // Unit head — the person a unit (and its person-less children: empty-unit
71
+ // placeholders, pending-invite ghosts, open-role seats) anchors under in the
72
+ // people chart. A presentation/anchor "tether" (ADR-BE-284), NOT authority.
73
+ // `origin` distinguishes an explicit user choice from a system-inferred default
74
+ // so the UI can render "Pinned" vs "Auto" and offer revert-to-auto. The client
75
+ // prefers the head of a unit's PARENT when anchoring its person-less children,
76
+ // falling back to the render-time heuristic only when no head row exists.
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export const UnitHeadOriginSchema = z.enum(["user", "inferred"]);
80
+ export type UnitHeadOrigin = z.infer<typeof UnitHeadOriginSchema>;
81
+
82
+ export const PeopleOrgChartUnitHeadSchema = z.object({
83
+ unitId: z.string().uuid(),
84
+ headUserId: z.string().uuid(),
85
+ origin: UnitHeadOriginSchema,
86
+ });
87
+
88
+ export type PeopleOrgChartUnitHead = z.infer<
89
+ typeof PeopleOrgChartUnitHeadSchema
90
+ >;
91
+
69
92
  // ---------------------------------------------------------------------------
70
93
  // GET /api/users/org-chart
71
94
  // ---------------------------------------------------------------------------
@@ -74,6 +97,7 @@ export const PeopleOrgChartResponseSchema = z.object({
74
97
  nodes: z.array(PeopleOrgChartNodeSchema),
75
98
  edges: z.array(PeopleOrgChartEdgeSchema),
76
99
  openRoles: z.array(PeopleOrgChartOpenRoleSchema),
100
+ unitHeads: z.array(PeopleOrgChartUnitHeadSchema),
77
101
  });
78
102
 
79
103
  export type PeopleOrgChartResponse = z.infer<
package/src/index.ts CHANGED
@@ -173,6 +173,8 @@ export {
173
173
  PeopleOrgChartNodeSchema,
174
174
  PeopleOrgChartEdgeSchema,
175
175
  PeopleOrgChartOpenRoleSchema,
176
+ UnitHeadOriginSchema,
177
+ PeopleOrgChartUnitHeadSchema,
176
178
  PeopleOrgChartResponseSchema,
177
179
  } from "./identity/index";
178
180
  export type {
@@ -180,6 +182,8 @@ export type {
180
182
  PeopleOrgChartNode,
181
183
  PeopleOrgChartEdge,
182
184
  PeopleOrgChartOpenRole,
185
+ UnitHeadOrigin,
186
+ PeopleOrgChartUnitHead,
183
187
  PeopleOrgChartResponse,
184
188
  } from "./identity/index";
185
189
 
@@ -98,6 +98,8 @@ describe("OrgUnitTreeNodeSchema", () => {
98
98
  memberCount: 5,
99
99
  openRoleCount: 1,
100
100
  missingAtNextLevel: null,
101
+ headUserId: null,
102
+ headOrigin: null,
101
103
  };
102
104
  expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
103
105
  expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 0 })).toThrow();
@@ -112,6 +114,8 @@ describe("OrgUnitTreeNodeSchema", () => {
112
114
  memberCount: 5,
113
115
  openRoleCount: 0,
114
116
  missingAtNextLevel: null,
117
+ headUserId: null,
118
+ headOrigin: null,
115
119
  };
116
120
  expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
117
121
  expect(() =>
@@ -947,6 +947,16 @@ export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
947
947
  */
948
948
  openRoleCount: z.number().int().min(0),
949
949
  missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
950
+ /**
951
+ * The unit head (ADR-BE-284): the person this unit (and its person-less
952
+ * children) anchors under in the people chart — a presentation "tether", NOT
953
+ * authority. `headUserId` is null when no head is resolved (e.g. an empty
954
+ * unit with no members). `headOrigin` is `'user'` for an explicit choice,
955
+ * `'inferred'` for a system default (so the management UI can show Pinned vs
956
+ * Auto and offer revert-to-auto), or null when there is no head.
957
+ */
958
+ headUserId: z.string().uuid().nullable(),
959
+ headOrigin: z.enum(["user", "inferred"]).nullable(),
950
960
  });
951
961
 
952
962
  export const OrgUnitMembershipSchema = z.object({
@@ -36,6 +36,11 @@ export type ResourceKey =
36
36
  | { type: "orgUnitAncestors"; orgId: string; unitId: string }
37
37
  | { type: "orgUnitMemberships"; orgId: string; unitId: string }
38
38
  | { type: "orgUnitPermissions"; orgId: string; unitId: string }
39
+ // Open roles seated in a unit (ADR-BE-277) — a per-unit list, unitId-scoped
40
+ // like memberships/permissions. Read in the unit members view and the
41
+ // org-chart card; refreshed as a graph target of orgTree on structural
42
+ // mutations (create / advance lifecycle / fill). See ADR-CONTRACTS-065.
43
+ | { type: "orgUnitOpenRoles"; orgId: string; unitId: string }
39
44
  // Org-unit owners list (ADR-CONTRACTS-052) — owners are an org-wide
40
45
  // projection, not a per-unit collection, so unitId is intentionally excluded.
41
46
  | { type: "orgUnitOwners"; orgId: string }
@@ -145,6 +150,7 @@ export function toQueryKey(key: ResourceKey): readonly string[] {
145
150
  case "orgUnitAncestors":
146
151
  case "orgUnitMemberships":
147
152
  case "orgUnitPermissions":
153
+ case "orgUnitOpenRoles":
148
154
  return [key.type, key.orgId, key.unitId] as const;
149
155
 
150
156
  // User-scoped
@@ -218,6 +224,7 @@ export function fromQueryKey(queryKey: readonly string[]): ResourceKey {
218
224
  orgUnitAncestors: "unitId",
219
225
  orgUnitMemberships: "unitId",
220
226
  orgUnitPermissions: "unitId",
227
+ orgUnitOpenRoles: "unitId",
221
228
  };
222
229
 
223
230
  if (type in identityFields) {