@company-semantics/contracts 52.0.0 → 53.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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +122 -69
- package/src/generated/openapi-routes.ts +3 -1
- package/src/identity/README.md +2 -2
- package/src/identity/__tests__/people-org-chart.test.ts +52 -17
- package/src/identity/__tests__/position-ref.test.ts +44 -0
- package/src/identity/index.ts +6 -2
- package/src/identity/people-org-chart.ts +26 -15
- package/src/identity/position-ref.ts +24 -0
- package/src/index.ts +21 -2
- package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +23 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +309 -259
- package/src/notifications/__tests__/monospace-budget.test.ts +75 -0
- package/src/notifications/renderers/README.md +8 -4
- package/src/notifications/renderers/ascii/README.md +75 -0
- package/src/notifications/renderers/ascii/__tests__/README.md +39 -0
- package/src/notifications/renderers/ascii/__tests__/layout.test.ts +228 -0
- package/src/notifications/renderers/ascii/chat.ts +179 -0
- package/src/notifications/renderers/ascii/cta.ts +57 -0
- package/src/notifications/renderers/ascii/geometry.ts +112 -0
- package/src/notifications/renderers/ascii/index.ts +40 -0
- package/src/notifications/renderers/ascii/keyvalue.ts +34 -0
- package/src/notifications/renderers/ascii/rule.ts +53 -0
- package/src/notifications/renderers/ascii/runs.ts +84 -0
- package/src/notifications/renderers/ascii/signature.ts +41 -0
- package/src/notifications/renderers/ascii/wrap.ts +96 -0
- package/src/notifications/renderers/brand.ts +12 -0
- package/src/notifications/renderers/email/chat.ts +62 -146
- package/src/notifications/renderers/email/constants.ts +17 -2
- package/src/notifications/renderers/email/cta.ts +8 -13
- package/src/notifications/renderers/email/render.ts +29 -5
- package/src/notifications/renderers/layout.ts +31 -0
- package/src/notifications/renderers/slack/README.md +135 -79
- package/src/notifications/renderers/slack/__tests__/README.md +3 -2
- package/src/notifications/renderers/slack/__tests__/index.test.ts +233 -93
- package/src/notifications/renderers/slack/blocks.ts +149 -0
- package/src/notifications/renderers/slack/chat.ts +167 -0
- package/src/notifications/renderers/slack/cta.ts +69 -0
- package/src/notifications/renderers/slack/index.ts +136 -229
- package/src/notifications/renderers/slack/message.ts +23 -0
- package/src/org/README.md +10 -2
- package/src/org/__tests__/org-units.test.ts +1 -1
- package/src/org/__tests__/set-seat-manager.test.ts +177 -0
- package/src/org/index.ts +20 -2
- package/src/org/reconciliation.ts +162 -0
- package/src/org/schemas.ts +43 -17
- package/src/identity/org-chart-actor.ts +0 -24
package/package.json
CHANGED
|
@@ -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 = 'b8fe0a02c492' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'b8fe0a02c49284e732adac870b9b0ff155865b53f5cc8571aa0eb77c4af78859' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -738,7 +738,7 @@ export interface paths {
|
|
|
738
738
|
patch: operations["changeMemberRole"];
|
|
739
739
|
trace?: never;
|
|
740
740
|
};
|
|
741
|
-
"/api/
|
|
741
|
+
"/api/reporting/positions/{positionId}/manager": {
|
|
742
742
|
parameters: {
|
|
743
743
|
query?: never;
|
|
744
744
|
header?: never;
|
|
@@ -746,14 +746,13 @@ export interface paths {
|
|
|
746
746
|
cookie?: never;
|
|
747
747
|
};
|
|
748
748
|
get?: never;
|
|
749
|
-
|
|
749
|
+
/** Set or clear a seat's solid-line manager (org-chart reporting edge) */
|
|
750
|
+
put: operations["setSeatManager"];
|
|
750
751
|
post?: never;
|
|
751
|
-
|
|
752
|
-
delete: operations["clearMemberManager"];
|
|
752
|
+
delete?: never;
|
|
753
753
|
options?: never;
|
|
754
754
|
head?: never;
|
|
755
|
-
|
|
756
|
-
patch: operations["setMemberManager"];
|
|
755
|
+
patch?: never;
|
|
757
756
|
trace?: never;
|
|
758
757
|
};
|
|
759
758
|
"/api/reporting/move/preview": {
|
|
@@ -1409,6 +1408,46 @@ export interface paths {
|
|
|
1409
1408
|
patch?: never;
|
|
1410
1409
|
trace?: never;
|
|
1411
1410
|
};
|
|
1411
|
+
"/api/org/reconciliation": {
|
|
1412
|
+
parameters: {
|
|
1413
|
+
query?: never;
|
|
1414
|
+
header?: never;
|
|
1415
|
+
path?: never;
|
|
1416
|
+
cookie?: never;
|
|
1417
|
+
};
|
|
1418
|
+
/**
|
|
1419
|
+
* List the workspace’s standing disagreements with the connected HRIS
|
|
1420
|
+
* @description The org’s still-open divergences, newest first, whether they arose during a sync run or during app use. Distinct from the run-scoped conflicts view, which reports what one run refused. Read-only: nothing here is sent to the HRIS.
|
|
1421
|
+
*/
|
|
1422
|
+
get: operations["getOrgReconciliation"];
|
|
1423
|
+
put?: never;
|
|
1424
|
+
post?: never;
|
|
1425
|
+
delete?: never;
|
|
1426
|
+
options?: never;
|
|
1427
|
+
head?: never;
|
|
1428
|
+
patch?: never;
|
|
1429
|
+
trace?: never;
|
|
1430
|
+
};
|
|
1431
|
+
"/api/org/reconciliation/divergences/{divergenceId}/resolution": {
|
|
1432
|
+
parameters: {
|
|
1433
|
+
query?: never;
|
|
1434
|
+
header?: never;
|
|
1435
|
+
path?: never;
|
|
1436
|
+
cookie?: never;
|
|
1437
|
+
};
|
|
1438
|
+
get?: never;
|
|
1439
|
+
put?: never;
|
|
1440
|
+
/**
|
|
1441
|
+
* Acknowledge a standing disagreement with the connected HRIS
|
|
1442
|
+
* @description Records LOCALLY that this workspace has seen the disagreement. Nothing is sent to the HRIS — the result’s sentToSource is always false — and the divergence stays open in the ledger, now carrying acknowledgedAt. Idempotent: acknowledging twice returns the first acknowledgement’s timestamp.
|
|
1443
|
+
*/
|
|
1444
|
+
post: operations["resolveOrgDivergence"];
|
|
1445
|
+
delete?: never;
|
|
1446
|
+
options?: never;
|
|
1447
|
+
head?: never;
|
|
1448
|
+
patch?: never;
|
|
1449
|
+
trace?: never;
|
|
1450
|
+
};
|
|
1412
1451
|
"/api/company-md/tree": {
|
|
1413
1452
|
parameters: {
|
|
1414
1453
|
query?: never;
|
|
@@ -4340,35 +4379,23 @@ export interface components {
|
|
|
4340
4379
|
/** @enum {string} */
|
|
4341
4380
|
newRole: "admin" | "member";
|
|
4342
4381
|
};
|
|
4343
|
-
|
|
4382
|
+
SetSeatManagerResponse: {
|
|
4344
4383
|
success: boolean;
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
kind: "member";
|
|
4349
|
-
/** Format: uuid */
|
|
4350
|
-
userId: string;
|
|
4351
|
-
} | {
|
|
4352
|
-
/** @constant */
|
|
4353
|
-
kind: "open-role";
|
|
4384
|
+
/** Format: uuid */
|
|
4385
|
+
reportPositionId: string;
|
|
4386
|
+
manager: {
|
|
4354
4387
|
/** Format: uuid */
|
|
4355
|
-
|
|
4356
|
-
}
|
|
4357
|
-
|
|
4388
|
+
positionId: string;
|
|
4389
|
+
} | null;
|
|
4390
|
+
/** @constant */
|
|
4391
|
+
relationshipType: "solid";
|
|
4358
4392
|
message: string;
|
|
4359
4393
|
};
|
|
4360
|
-
|
|
4394
|
+
SetSeatManagerRequest: {
|
|
4361
4395
|
manager: {
|
|
4362
|
-
/** @constant */
|
|
4363
|
-
kind: "member";
|
|
4364
|
-
/** Format: uuid */
|
|
4365
|
-
userId: string;
|
|
4366
|
-
} | {
|
|
4367
|
-
/** @constant */
|
|
4368
|
-
kind: "open-role";
|
|
4369
4396
|
/** Format: uuid */
|
|
4370
|
-
|
|
4371
|
-
};
|
|
4397
|
+
positionId: string;
|
|
4398
|
+
} | null;
|
|
4372
4399
|
};
|
|
4373
4400
|
MovePreviewRequest: {
|
|
4374
4401
|
/** @constant */
|
|
@@ -4934,6 +4961,10 @@ export interface components {
|
|
|
4934
4961
|
/** @enum {string} */
|
|
4935
4962
|
decision: "accept" | "reject";
|
|
4936
4963
|
};
|
|
4964
|
+
ResolveOrgDivergenceRequest: {
|
|
4965
|
+
/** @constant */
|
|
4966
|
+
resolution: "acknowledged";
|
|
4967
|
+
};
|
|
4937
4968
|
CompanyMdListResponse: {
|
|
4938
4969
|
items: {
|
|
4939
4970
|
id: string;
|
|
@@ -5673,7 +5704,7 @@ export interface components {
|
|
|
5673
5704
|
/** @enum {string} */
|
|
5674
5705
|
status: "open" | "hiring" | "filled" | "closed";
|
|
5675
5706
|
filledByUserId: string | null;
|
|
5676
|
-
|
|
5707
|
+
reportsToPositionId: string | null;
|
|
5677
5708
|
createdAt: string;
|
|
5678
5709
|
updatedAt: string;
|
|
5679
5710
|
}[];
|
|
@@ -5691,7 +5722,7 @@ export interface components {
|
|
|
5691
5722
|
/** @enum {string} */
|
|
5692
5723
|
status: "open" | "hiring" | "filled" | "closed";
|
|
5693
5724
|
filledByUserId: string | null;
|
|
5694
|
-
|
|
5725
|
+
reportsToPositionId: string | null;
|
|
5695
5726
|
createdAt: string;
|
|
5696
5727
|
updatedAt: string;
|
|
5697
5728
|
};
|
|
@@ -5708,7 +5739,8 @@ export interface components {
|
|
|
5708
5739
|
hiring: boolean;
|
|
5709
5740
|
occupants: {
|
|
5710
5741
|
/** Format: uuid */
|
|
5711
|
-
|
|
5742
|
+
personId: string;
|
|
5743
|
+
userId: string | null;
|
|
5712
5744
|
fullName: string;
|
|
5713
5745
|
avatarUrl: string | null;
|
|
5714
5746
|
primaryUnitId: string | null;
|
|
@@ -5720,6 +5752,7 @@ export interface components {
|
|
|
5720
5752
|
mutability: "derived" | "user_managed";
|
|
5721
5753
|
}[];
|
|
5722
5754
|
}[];
|
|
5755
|
+
integrity: "canonical_placement_conflict" | null;
|
|
5723
5756
|
}[];
|
|
5724
5757
|
seatEdges: {
|
|
5725
5758
|
/** Format: uuid */
|
|
@@ -7926,50 +7959,28 @@ export interface operations {
|
|
|
7926
7959
|
};
|
|
7927
7960
|
};
|
|
7928
7961
|
};
|
|
7929
|
-
|
|
7930
|
-
parameters: {
|
|
7931
|
-
query?: never;
|
|
7932
|
-
header?: never;
|
|
7933
|
-
path: {
|
|
7934
|
-
id: string;
|
|
7935
|
-
};
|
|
7936
|
-
cookie?: never;
|
|
7937
|
-
};
|
|
7938
|
-
requestBody?: never;
|
|
7939
|
-
responses: {
|
|
7940
|
-
/** @description Manager cleared successfully */
|
|
7941
|
-
200: {
|
|
7942
|
-
headers: {
|
|
7943
|
-
[name: string]: unknown;
|
|
7944
|
-
};
|
|
7945
|
-
content: {
|
|
7946
|
-
"application/json": components["schemas"]["SetMemberManagerResponse"];
|
|
7947
|
-
};
|
|
7948
|
-
};
|
|
7949
|
-
};
|
|
7950
|
-
};
|
|
7951
|
-
setMemberManager: {
|
|
7962
|
+
setSeatManager: {
|
|
7952
7963
|
parameters: {
|
|
7953
7964
|
query?: never;
|
|
7954
7965
|
header?: never;
|
|
7955
7966
|
path: {
|
|
7956
|
-
|
|
7967
|
+
positionId: string;
|
|
7957
7968
|
};
|
|
7958
7969
|
cookie?: never;
|
|
7959
7970
|
};
|
|
7960
7971
|
requestBody: {
|
|
7961
7972
|
content: {
|
|
7962
|
-
"application/json": components["schemas"]["
|
|
7973
|
+
"application/json": components["schemas"]["SetSeatManagerRequest"];
|
|
7963
7974
|
};
|
|
7964
7975
|
};
|
|
7965
7976
|
responses: {
|
|
7966
|
-
/** @description
|
|
7977
|
+
/** @description The seat's solid-line manager edge was set or cleared */
|
|
7967
7978
|
200: {
|
|
7968
7979
|
headers: {
|
|
7969
7980
|
[name: string]: unknown;
|
|
7970
7981
|
};
|
|
7971
7982
|
content: {
|
|
7972
|
-
"application/json": components["schemas"]["
|
|
7983
|
+
"application/json": components["schemas"]["SetSeatManagerResponse"];
|
|
7973
7984
|
};
|
|
7974
7985
|
};
|
|
7975
7986
|
};
|
|
@@ -8898,6 +8909,55 @@ export interface operations {
|
|
|
8898
8909
|
};
|
|
8899
8910
|
};
|
|
8900
8911
|
};
|
|
8912
|
+
getOrgReconciliation: {
|
|
8913
|
+
parameters: {
|
|
8914
|
+
query?: never;
|
|
8915
|
+
header?: never;
|
|
8916
|
+
path?: never;
|
|
8917
|
+
cookie?: never;
|
|
8918
|
+
};
|
|
8919
|
+
requestBody?: never;
|
|
8920
|
+
responses: {
|
|
8921
|
+
/** @description The org’s open divergences, newest first — each carrying both sides, the winning tier and the human-readable reason (contracts OrgReconciliationLedger) */
|
|
8922
|
+
200: {
|
|
8923
|
+
headers: {
|
|
8924
|
+
[name: string]: unknown;
|
|
8925
|
+
};
|
|
8926
|
+
content?: never;
|
|
8927
|
+
};
|
|
8928
|
+
};
|
|
8929
|
+
};
|
|
8930
|
+
resolveOrgDivergence: {
|
|
8931
|
+
parameters: {
|
|
8932
|
+
query?: never;
|
|
8933
|
+
header?: never;
|
|
8934
|
+
path: {
|
|
8935
|
+
divergenceId: string;
|
|
8936
|
+
};
|
|
8937
|
+
cookie?: never;
|
|
8938
|
+
};
|
|
8939
|
+
requestBody: {
|
|
8940
|
+
content: {
|
|
8941
|
+
"application/json": components["schemas"]["ResolveOrgDivergenceRequest"];
|
|
8942
|
+
};
|
|
8943
|
+
};
|
|
8944
|
+
responses: {
|
|
8945
|
+
/** @description The acknowledgement, with sentToSource pinned false (contracts ResolveOrgDivergenceResult) */
|
|
8946
|
+
200: {
|
|
8947
|
+
headers: {
|
|
8948
|
+
[name: string]: unknown;
|
|
8949
|
+
};
|
|
8950
|
+
content?: never;
|
|
8951
|
+
};
|
|
8952
|
+
/** @description No open divergence with that id — it belongs to another workspace, a later statement superseded it, or it has since been settled */
|
|
8953
|
+
404: {
|
|
8954
|
+
headers: {
|
|
8955
|
+
[name: string]: unknown;
|
|
8956
|
+
};
|
|
8957
|
+
content?: never;
|
|
8958
|
+
};
|
|
8959
|
+
};
|
|
8960
|
+
};
|
|
8901
8961
|
getCompanyMdTree: {
|
|
8902
8962
|
parameters: {
|
|
8903
8963
|
query?: {
|
|
@@ -10848,17 +10908,10 @@ export interface operations {
|
|
|
10848
10908
|
requestBody: {
|
|
10849
10909
|
content: {
|
|
10850
10910
|
"application/json": {
|
|
10851
|
-
manager:
|
|
10852
|
-
/** @constant */
|
|
10853
|
-
kind: "member";
|
|
10854
|
-
/** Format: uuid */
|
|
10855
|
-
userId: string;
|
|
10856
|
-
} | {
|
|
10857
|
-
/** @constant */
|
|
10858
|
-
kind: "open-role";
|
|
10911
|
+
manager: {
|
|
10859
10912
|
/** Format: uuid */
|
|
10860
|
-
|
|
10861
|
-
}
|
|
10913
|
+
positionId: string;
|
|
10914
|
+
} | null;
|
|
10862
10915
|
};
|
|
10863
10916
|
};
|
|
10864
10917
|
};
|
|
@@ -138,6 +138,8 @@ export const openApiRoutes = {
|
|
|
138
138
|
'/api/org/cancel-deletion': ['POST'],
|
|
139
139
|
'/api/org/delete': ['POST'],
|
|
140
140
|
'/api/org/deletion-eligibility': ['GET'],
|
|
141
|
+
'/api/org/reconciliation': ['GET'],
|
|
142
|
+
'/api/org/reconciliation/divergences/{divergenceId}/resolution': ['POST'],
|
|
141
143
|
'/api/org/system-events': ['GET'],
|
|
142
144
|
'/api/org/system-events/{id}/acknowledge': ['POST'],
|
|
143
145
|
'/api/org/transfer-eligibility': ['GET'],
|
|
@@ -154,6 +156,7 @@ export const openApiRoutes = {
|
|
|
154
156
|
'/api/rbac/roles': ['GET'],
|
|
155
157
|
'/api/reporting/move/apply': ['POST'],
|
|
156
158
|
'/api/reporting/move/preview': ['POST'],
|
|
159
|
+
'/api/reporting/positions/{positionId}/manager': ['PUT'],
|
|
157
160
|
'/api/scope/check': ['GET'],
|
|
158
161
|
'/api/scope/check-batch': ['POST'],
|
|
159
162
|
'/api/shared/{token}': ['GET'],
|
|
@@ -192,7 +195,6 @@ export const openApiRoutes = {
|
|
|
192
195
|
'/api/workspace/invites/{id}': ['DELETE'],
|
|
193
196
|
'/api/workspace/members': ['GET'],
|
|
194
197
|
'/api/workspace/members/{id}': ['DELETE', 'GET'],
|
|
195
|
-
'/api/workspace/members/{id}/manager': ['DELETE', 'PATCH'],
|
|
196
198
|
'/api/workspace/members/{id}/role': ['PATCH'],
|
|
197
199
|
'/api/workspace/name': ['PATCH'],
|
|
198
200
|
'/api/workspace/organization': ['PATCH'],
|
package/src/identity/README.md
CHANGED
|
@@ -49,8 +49,6 @@ TypeScript types and functions for user identity and display name resolution.
|
|
|
49
49
|
- `MeResponse` _(type)_
|
|
50
50
|
- `MeResponseSchema` — Full identity context returned by GET /api/me.
|
|
51
51
|
- `NameSource` _(type)_ — Source of the user's name data.
|
|
52
|
-
- `OrgChartActorRef` _(type)_
|
|
53
|
-
- `OrgChartActorRefSchema`
|
|
54
52
|
- `PeopleOrgChartOwnedUnit` _(type)_
|
|
55
53
|
- `PeopleOrgChartOwnedUnitSchema`
|
|
56
54
|
- `PeopleOrgChartResponse` _(type)_
|
|
@@ -65,6 +63,8 @@ TypeScript types and functions for user identity and display name resolution.
|
|
|
65
63
|
- `PeopleOrgChartSeatStatusSchema`
|
|
66
64
|
- `Person` _(type)_
|
|
67
65
|
- `PersonSchema`
|
|
66
|
+
- `PositionRef` _(type)_
|
|
67
|
+
- `PositionRefSchema` — A reference to a POSITION in the org graph.
|
|
68
68
|
- `ProfileResponse` _(type)_
|
|
69
69
|
- `ProfileResponseSchema`
|
|
70
70
|
- `ReportingRelationshipType` _(type)_
|
|
@@ -51,25 +51,60 @@ describe("PeopleOrgChartSeatOccupantSchema", () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
describe("PeopleOrgChartSeatSchema", () => {
|
|
54
|
+
const filledSeat = {
|
|
55
|
+
id: POSITION_ID,
|
|
56
|
+
unitId: UNIT_ID,
|
|
57
|
+
title: "Analyst",
|
|
58
|
+
status: "filled",
|
|
59
|
+
hiring: false,
|
|
60
|
+
occupants: [
|
|
61
|
+
{
|
|
62
|
+
personId: PERSON_ID,
|
|
63
|
+
userId: null,
|
|
64
|
+
fullName: "Ada Lovelace",
|
|
65
|
+
avatarUrl: null,
|
|
66
|
+
primaryUnitId: null,
|
|
67
|
+
ownedUnits: [],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
integrity: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
54
73
|
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
|
-
});
|
|
74
|
+
const seat = PeopleOrgChartSeatSchema.parse(filledSeat);
|
|
72
75
|
expect(seat.occupants).toHaveLength(1);
|
|
73
76
|
expect(seat.occupants[0]?.userId).toBeNull();
|
|
77
|
+
expect(seat.integrity).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// The marker is what separates a breached seat from a vacancy: both carry
|
|
81
|
+
// `occupants: []`, so a consumer reading only the occupant list would offer
|
|
82
|
+
// to fill a seat whose holder the server deliberately withheld.
|
|
83
|
+
it("admits a seat whose withheld holder is flagged as a placement conflict", () => {
|
|
84
|
+
const seat = PeopleOrgChartSeatSchema.parse({
|
|
85
|
+
...filledSeat,
|
|
86
|
+
occupants: [],
|
|
87
|
+
integrity: "canonical_placement_conflict",
|
|
88
|
+
});
|
|
89
|
+
expect(seat.integrity).toBe("canonical_placement_conflict");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// .nullable() WITHOUT .optional(), for the same reason as occupant.userId:
|
|
93
|
+
// the server always emits the key, so "no marker" must be spelled null rather
|
|
94
|
+
// than left to an absent field a consumer would read as healthy by default.
|
|
95
|
+
it("rejects an absent integrity — nullable is not optional", () => {
|
|
96
|
+
const { integrity: _omitted, ...withoutIntegrity } = filledSeat;
|
|
97
|
+
expect(PeopleOrgChartSeatSchema.safeParse(withoutIntegrity).success).toBe(
|
|
98
|
+
false,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects an unknown integrity marker", () => {
|
|
103
|
+
expect(
|
|
104
|
+
PeopleOrgChartSeatSchema.safeParse({
|
|
105
|
+
...filledSeat,
|
|
106
|
+
integrity: "something_else",
|
|
107
|
+
}).success,
|
|
108
|
+
).toBe(false);
|
|
74
109
|
});
|
|
75
110
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { PositionRefSchema } from "../position-ref.js";
|
|
4
|
+
|
|
5
|
+
const POSITION_ID = "11111111-1111-4111-8111-111111111111";
|
|
6
|
+
const USER_ID = "22222222-2222-4222-8222-222222222222";
|
|
7
|
+
|
|
8
|
+
describe("PositionRefSchema", () => {
|
|
9
|
+
it("accepts a reference carrying a uuid positionId", () => {
|
|
10
|
+
expect(PositionRefSchema.parse({ positionId: POSITION_ID })).toEqual({
|
|
11
|
+
positionId: POSITION_ID,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("rejects a non-uuid positionId", () => {
|
|
16
|
+
expect(PositionRefSchema.safeParse({ positionId: "seat-1" }).success).toBe(
|
|
17
|
+
false,
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects an absent positionId", () => {
|
|
22
|
+
expect(PositionRefSchema.safeParse({}).success).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// The retired OrgChartActorRef union spelled a filled seat as
|
|
26
|
+
// `{ kind: 'member', userId }`. Nothing in the write vocabulary speaks that
|
|
27
|
+
// shape any more: a user id is not a way to name a chart actor, because most
|
|
28
|
+
// people in an org graph have no account at all.
|
|
29
|
+
it("does not admit the retired member arm — a userId is not a position", () => {
|
|
30
|
+
const result = PositionRefSchema.safeParse({
|
|
31
|
+
kind: "member",
|
|
32
|
+
userId: USER_ID,
|
|
33
|
+
});
|
|
34
|
+
expect(result.success).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Not strict(): unknown keys are stripped rather than rejected, matching the
|
|
38
|
+
// rest of the identity vocabulary.
|
|
39
|
+
it("strips unknown keys rather than rejecting them", () => {
|
|
40
|
+
expect(
|
|
41
|
+
PositionRefSchema.parse({ positionId: POSITION_ID, kind: "open-role" }),
|
|
42
|
+
).toEqual({ positionId: POSITION_ID });
|
|
43
|
+
});
|
|
44
|
+
});
|
package/src/identity/index.ts
CHANGED
|
@@ -64,7 +64,6 @@ export type {
|
|
|
64
64
|
// People Org Chart (GET /api/users/org-chart)
|
|
65
65
|
export {
|
|
66
66
|
ReportingRelationshipTypeSchema,
|
|
67
|
-
OrgChartActorRefSchema,
|
|
68
67
|
PeopleOrgChartOwnedUnitSchema,
|
|
69
68
|
PeopleOrgChartSeatStatusSchema,
|
|
70
69
|
PeopleOrgChartSeatOccupantSchema,
|
|
@@ -74,7 +73,6 @@ export {
|
|
|
74
73
|
} from "./people-org-chart";
|
|
75
74
|
export type {
|
|
76
75
|
ReportingRelationshipType,
|
|
77
|
-
OrgChartActorRef,
|
|
78
76
|
PeopleOrgChartOwnedUnit,
|
|
79
77
|
PeopleOrgChartSeatStatus,
|
|
80
78
|
PeopleOrgChartSeatOccupant,
|
|
@@ -83,6 +81,12 @@ export type {
|
|
|
83
81
|
PeopleOrgChartResponse,
|
|
84
82
|
} from "./people-org-chart";
|
|
85
83
|
|
|
84
|
+
// Position Ref — the single org-graph WRITE reference (ADR-CONTRACTS-131).
|
|
85
|
+
// Not chart vocabulary: three write schemas outside the chart speak it, which
|
|
86
|
+
// is why it is named for the Position and exported from its own module.
|
|
87
|
+
export { PositionRefSchema } from "./position-ref";
|
|
88
|
+
export type { PositionRef } from "./position-ref";
|
|
89
|
+
|
|
86
90
|
// Person — the org-graph node for a human (0..1 User link), per ADR-CTRL-182
|
|
87
91
|
export { PersonSchema } from "./person";
|
|
88
92
|
export type { Person } from "./person";
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* The chart is position-centric (ADR-CONT-093/094): every actor is a `seat`
|
|
8
8
|
* (a `positions` row) carrying its seat-scoped attributes plus an `occupants[]`
|
|
9
9
|
* array; reporting is expressed seat→seat in `seatEdges[]`. A VACANT seat (no
|
|
10
|
-
* occupants) is the old "open role"; an OCCUPIED
|
|
10
|
+
* occupants AND a null `integrity` marker) is the old "open role"; an OCCUPIED
|
|
11
|
+
* seat is the old "member". A seat whose `integrity` marker is set is neither:
|
|
12
|
+
* its holder exists but was withheld, so it must never be offered as an open
|
|
13
|
+
* role.
|
|
11
14
|
*
|
|
12
15
|
* Person-level reporting is intentionally distinct from
|
|
13
16
|
* `org_unit_relationships.type = 'reports_to'` (unit-to-unit graph overlay,
|
|
@@ -25,14 +28,6 @@ export type ReportingRelationshipType = z.infer<
|
|
|
25
28
|
typeof ReportingRelationshipTypeSchema
|
|
26
29
|
>;
|
|
27
30
|
|
|
28
|
-
// Actor reference (member | open-role). Retained WRITE-ONLY: the set-manager
|
|
29
|
-
// request/response (`org/schemas`) still speaks this vocabulary; the read model
|
|
30
|
-
// is now seat→seat (ADR-CONT-094) and no longer references it. Defined in its
|
|
31
|
-
// own zero-dependency module to avoid a module-init cycle with `org/schemas`,
|
|
32
|
-
// and re-exported here so the existing barrel import site keeps working.
|
|
33
|
-
export { OrgChartActorRefSchema } from "./org-chart-actor";
|
|
34
|
-
export type { OrgChartActorRef } from "./org-chart-actor";
|
|
35
|
-
|
|
36
31
|
// ---------------------------------------------------------------------------
|
|
37
32
|
// Owned unit — shared by the seat occupant (below)
|
|
38
33
|
// ---------------------------------------------------------------------------
|
|
@@ -67,13 +62,14 @@ export type PeopleOrgChartOwnedUnit = z.infer<
|
|
|
67
62
|
// its SEAT-SCOPED attributes (unit placement, title, seat-existence status,
|
|
68
63
|
// hiring/staffing sub-state) plus an `occupants[]` array of PERSON-SCOPED
|
|
69
64
|
// attributes for whoever currently holds it. The split is structural, not
|
|
70
|
-
// member-vs-open-role: a VACANT seat (`occupants: []`
|
|
65
|
+
// member-vs-open-role: a VACANT seat (`occupants: []` with `integrity: null`)
|
|
66
|
+
// is the old "open role";
|
|
71
67
|
// an OCCUPIED seat (`occupants.length >= 1`) is the old "member" (N occupants
|
|
72
68
|
// for a co-occupied seat — forward-compatible; live data is 0/1). Reporting is
|
|
73
69
|
// expressed seat→seat in `seatEdges[]`, NOT on the seat, so the edge list stays
|
|
74
|
-
// the single source of truth for placement (ADR-CTRL-159).
|
|
75
|
-
//
|
|
76
|
-
//
|
|
70
|
+
// the single source of truth for placement (ADR-CTRL-159). The write model
|
|
71
|
+
// names the same actors with a `PositionRef` (`identity/position-ref`), so read
|
|
72
|
+
// and write now agree that a chart actor IS a position.
|
|
77
73
|
//
|
|
78
74
|
// `seats[]` + `seatEdges[]` are now the ONLY shape — the legacy
|
|
79
75
|
// `nodes`/`edges`/`openRoles` half was removed in ADR-CONT-094 (breaking).
|
|
@@ -144,7 +140,22 @@ export const PeopleOrgChartSeatSchema = z.object({
|
|
|
144
140
|
// The seat's current holders. [] ⇒ vacant (the old "open role"); >= 1 ⇒
|
|
145
141
|
// occupied (the old "member"). Every canonical occupant appears, account or
|
|
146
142
|
// not — occupancy, not account linkage, is what admits a person here.
|
|
143
|
+
//
|
|
144
|
+
// READ WITH `integrity` BELOW: `occupants: []` means vacant ONLY when
|
|
145
|
+
// `integrity` is null.
|
|
147
146
|
occupants: z.array(PeopleOrgChartSeatOccupantSchema),
|
|
147
|
+
// Structural integrity of the OCCUPANT LIST above. null ⇒ the list is
|
|
148
|
+
// complete and `occupants: []` is a true vacancy.
|
|
149
|
+
//
|
|
150
|
+
// `canonical_placement_conflict` ⇒ at least one person holding this seat was
|
|
151
|
+
// WITHHELD from `occupants` because they hold more than one live canonical
|
|
152
|
+
// placement (ADR-BE-559). The projector refuses to bless one of the
|
|
153
|
+
// conflicting placements, so the seat renders with its holder unnamed — and
|
|
154
|
+
// an EMPTY `occupants` on such a seat is NOT a vacancy. A consumer must not
|
|
155
|
+
// offer to fill, staff, or reorg a flagged seat as an open role: the seat has
|
|
156
|
+
// a holder the server declines to name, and the breach is recorded
|
|
157
|
+
// server-side as `org_chart.canonical_placement_conflict`.
|
|
158
|
+
integrity: z.enum(["canonical_placement_conflict"]).nullable(),
|
|
148
159
|
});
|
|
149
160
|
|
|
150
161
|
export type PeopleOrgChartSeat = z.infer<typeof PeopleOrgChartSeatSchema>;
|
|
@@ -152,8 +163,8 @@ export type PeopleOrgChartSeat = z.infer<typeof PeopleOrgChartSeatSchema>;
|
|
|
152
163
|
// A seat→seat reporting edge — one row per `position_reporting` row (NOT a
|
|
153
164
|
// member cross-product). Endpoints are positionIds; resolve each to its seat in
|
|
154
165
|
// `seats[]`. Mirrors the legacy person-keyed `PeopleOrgChartEdge`, but keyed by
|
|
155
|
-
// the stable seat identity so a vacant seat's edge is
|
|
156
|
-
// `
|
|
166
|
+
// the stable seat identity so a vacant or accountless seat's edge is
|
|
167
|
+
// expressible at all — the same identity the write model spells `PositionRef`.
|
|
157
168
|
export const PeopleOrgChartSeatEdgeSchema = z.object({
|
|
158
169
|
reportPositionId: z.string().uuid(),
|
|
159
170
|
managerPositionId: z.string().uuid(),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A reference to a POSITION in the org graph.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the two-arm OrgChartActorRef union, which was already degenerate:
|
|
7
|
+
* `openRoleId` WAS a positions.id, and `{ kind:'member', userId }` was only a
|
|
8
|
+
* slow way to name a seat -- every consumer immediately resolved it through
|
|
9
|
+
* `resolveSeat`. Once occupants are person-keyed the `member` arm has no
|
|
10
|
+
* definition at all, because most people in an org graph have no account.
|
|
11
|
+
*
|
|
12
|
+
* Named PositionRef, not OrgChartSeatRef: it is consumed by the member-manager
|
|
13
|
+
* schema, the open-role reports-to schema and the reporting route, so it
|
|
14
|
+
* already spreads well past the chart. The thing referenced is a Position.
|
|
15
|
+
*
|
|
16
|
+
* An OBJECT rather than a bare uuid so `{ manager: PositionRef | null }` keeps
|
|
17
|
+
* `null` meaning "clear", the field name says what the uuid is, and future edge
|
|
18
|
+
* attributes have a home.
|
|
19
|
+
*
|
|
20
|
+
* Zero-dependency (zod only) to avoid the module-init cycle that caused the
|
|
21
|
+
* original actor-ref file to be split out.
|
|
22
|
+
*/
|
|
23
|
+
export const PositionRefSchema = z.object({ positionId: z.string().uuid() });
|
|
24
|
+
export type PositionRef = z.infer<typeof PositionRefSchema>;
|
package/src/index.ts
CHANGED
|
@@ -175,7 +175,6 @@ export type {
|
|
|
175
175
|
// People Org Chart (GET /api/users/org-chart)
|
|
176
176
|
export {
|
|
177
177
|
ReportingRelationshipTypeSchema,
|
|
178
|
-
OrgChartActorRefSchema,
|
|
179
178
|
PeopleOrgChartOwnedUnitSchema,
|
|
180
179
|
PeopleOrgChartSeatStatusSchema,
|
|
181
180
|
PeopleOrgChartSeatOccupantSchema,
|
|
@@ -185,7 +184,6 @@ export {
|
|
|
185
184
|
} from "./identity/index";
|
|
186
185
|
export type {
|
|
187
186
|
ReportingRelationshipType,
|
|
188
|
-
OrgChartActorRef,
|
|
189
187
|
PeopleOrgChartOwnedUnit,
|
|
190
188
|
PeopleOrgChartSeatStatus,
|
|
191
189
|
PeopleOrgChartSeatOccupant,
|
|
@@ -194,6 +192,10 @@ export type {
|
|
|
194
192
|
PeopleOrgChartResponse,
|
|
195
193
|
} from "./identity/index";
|
|
196
194
|
|
|
195
|
+
// Position Ref — the single org-graph WRITE reference (ADR-CONTRACTS-131)
|
|
196
|
+
export { PositionRefSchema } from "./identity/index";
|
|
197
|
+
export type { PositionRef } from "./identity/index";
|
|
198
|
+
|
|
197
199
|
// Person — the org-graph node for a human (0..1 User link), per ADR-CTRL-182
|
|
198
200
|
export { PersonSchema } from "./identity/index";
|
|
199
201
|
export type { Person } from "./identity/index";
|
|
@@ -681,6 +683,23 @@ export type {
|
|
|
681
683
|
OrgStructureInferenceDecisionResult,
|
|
682
684
|
} from "./org/index";
|
|
683
685
|
|
|
686
|
+
// Org reconciliation ledger: the org's STANDING disagreements with the connected
|
|
687
|
+
// HRIS — the open ones only, sync-time (runId set) and app-time (runId null)
|
|
688
|
+
// alike. Complements the run-scoped conflicts view rather than replacing it, and
|
|
689
|
+
// resolution is a LOCAL decision only. (ADR-BE-564)
|
|
690
|
+
export {
|
|
691
|
+
OrgDivergenceSchema,
|
|
692
|
+
OrgReconciliationLedgerSchema,
|
|
693
|
+
OrgDivergenceResolutionSchema,
|
|
694
|
+
ResolveOrgDivergenceResultSchema,
|
|
695
|
+
} from "./org/index";
|
|
696
|
+
export type {
|
|
697
|
+
OrgDivergence,
|
|
698
|
+
OrgReconciliationLedger,
|
|
699
|
+
OrgDivergenceResolution,
|
|
700
|
+
ResolveOrgDivergenceResult,
|
|
701
|
+
} from "./org/index";
|
|
702
|
+
|
|
684
703
|
// Org transformation wire vocabulary: intent -> simulation -> preview. The
|
|
685
704
|
// engine turns a TransformationRequest into scored mutation bundles and returns
|
|
686
705
|
// a TransformationPreview the operator confirms before apply. (ADR-CONTRACTS-072)
|