@company-semantics/contracts 19.2.0 → 20.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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +34 -34
- package/src/index.ts +30 -0
- package/src/org/README.md +16 -0
- package/src/org/__tests__/canonical-facts.test.ts +270 -0
- package/src/org/__tests__/conflict-record.test.ts +94 -0
- package/src/org/__tests__/sync-run.test.ts +69 -0
- package/src/org/canonical-facts.ts +181 -0
- package/src/org/conflict-record.ts +76 -0
- package/src/org/index.ts +30 -0
- package/src/org/sync-run.ts +57 -0
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 = '79972d6725ff' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '79972d6725ff3dfd29ee68945c8006039eddc196eadaeb0bcd77abeeebf2b066' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -1338,14 +1338,14 @@ export interface paths {
|
|
|
1338
1338
|
patch?: never;
|
|
1339
1339
|
trace?: never;
|
|
1340
1340
|
};
|
|
1341
|
-
"/api/company-md/docs/{
|
|
1341
|
+
"/api/company-md/docs/{id}": {
|
|
1342
1342
|
parameters: {
|
|
1343
1343
|
query?: never;
|
|
1344
1344
|
header?: never;
|
|
1345
1345
|
path?: never;
|
|
1346
1346
|
cookie?: never;
|
|
1347
1347
|
};
|
|
1348
|
-
/** Get a company.md document by
|
|
1348
|
+
/** Get a company.md document by id */
|
|
1349
1349
|
get: operations["getCompanyMdDoc"];
|
|
1350
1350
|
put?: never;
|
|
1351
1351
|
post?: never;
|
|
@@ -1355,7 +1355,7 @@ export interface paths {
|
|
|
1355
1355
|
patch?: never;
|
|
1356
1356
|
trace?: never;
|
|
1357
1357
|
};
|
|
1358
|
-
"/api/company-md/docs/{
|
|
1358
|
+
"/api/company-md/docs/{id}/content": {
|
|
1359
1359
|
parameters: {
|
|
1360
1360
|
query?: never;
|
|
1361
1361
|
header?: never;
|
|
@@ -1372,7 +1372,7 @@ export interface paths {
|
|
|
1372
1372
|
patch?: never;
|
|
1373
1373
|
trace?: never;
|
|
1374
1374
|
};
|
|
1375
|
-
"/api/company-md/docs/{
|
|
1375
|
+
"/api/company-md/docs/{id}/title": {
|
|
1376
1376
|
parameters: {
|
|
1377
1377
|
query?: never;
|
|
1378
1378
|
header?: never;
|
|
@@ -1406,7 +1406,7 @@ export interface paths {
|
|
|
1406
1406
|
patch?: never;
|
|
1407
1407
|
trace?: never;
|
|
1408
1408
|
};
|
|
1409
|
-
"/api/company-md/docs/{
|
|
1409
|
+
"/api/company-md/docs/{id}/context-bank": {
|
|
1410
1410
|
parameters: {
|
|
1411
1411
|
query?: never;
|
|
1412
1412
|
header?: never;
|
|
@@ -1440,7 +1440,7 @@ export interface paths {
|
|
|
1440
1440
|
patch?: never;
|
|
1441
1441
|
trace?: never;
|
|
1442
1442
|
};
|
|
1443
|
-
"/api/company-md/docs/{
|
|
1443
|
+
"/api/company-md/docs/{id}/context-bank/associate": {
|
|
1444
1444
|
parameters: {
|
|
1445
1445
|
query?: never;
|
|
1446
1446
|
header?: never;
|
|
@@ -1457,7 +1457,7 @@ export interface paths {
|
|
|
1457
1457
|
patch?: never;
|
|
1458
1458
|
trace?: never;
|
|
1459
1459
|
};
|
|
1460
|
-
"/api/company-md/docs/{
|
|
1460
|
+
"/api/company-md/docs/{id}/context-bank/{contextDocId}": {
|
|
1461
1461
|
parameters: {
|
|
1462
1462
|
query?: never;
|
|
1463
1463
|
header?: never;
|
|
@@ -1474,7 +1474,7 @@ export interface paths {
|
|
|
1474
1474
|
patch?: never;
|
|
1475
1475
|
trace?: never;
|
|
1476
1476
|
};
|
|
1477
|
-
"/api/company-md/docs/{
|
|
1477
|
+
"/api/company-md/docs/{id}/context-bank/{contextDocId}/order": {
|
|
1478
1478
|
parameters: {
|
|
1479
1479
|
query?: never;
|
|
1480
1480
|
header?: never;
|
|
@@ -1491,7 +1491,7 @@ export interface paths {
|
|
|
1491
1491
|
patch: operations["reorderCompanyMdContextBankItem"];
|
|
1492
1492
|
trace?: never;
|
|
1493
1493
|
};
|
|
1494
|
-
"/api/company-md/docs/{
|
|
1494
|
+
"/api/company-md/docs/{id}/context-bank/upload": {
|
|
1495
1495
|
parameters: {
|
|
1496
1496
|
query?: never;
|
|
1497
1497
|
header?: never;
|
|
@@ -1508,7 +1508,7 @@ export interface paths {
|
|
|
1508
1508
|
patch?: never;
|
|
1509
1509
|
trace?: never;
|
|
1510
1510
|
};
|
|
1511
|
-
"/api/company-md/docs/{
|
|
1511
|
+
"/api/company-md/docs/{id}/context-bank/retry": {
|
|
1512
1512
|
parameters: {
|
|
1513
1513
|
query?: never;
|
|
1514
1514
|
header?: never;
|
|
@@ -1525,7 +1525,7 @@ export interface paths {
|
|
|
1525
1525
|
patch?: never;
|
|
1526
1526
|
trace?: never;
|
|
1527
1527
|
};
|
|
1528
|
-
"/api/company-md/docs/{
|
|
1528
|
+
"/api/company-md/docs/{id}/transfer-owner": {
|
|
1529
1529
|
parameters: {
|
|
1530
1530
|
query?: never;
|
|
1531
1531
|
header?: never;
|
|
@@ -1593,7 +1593,7 @@ export interface paths {
|
|
|
1593
1593
|
patch?: never;
|
|
1594
1594
|
trace?: never;
|
|
1595
1595
|
};
|
|
1596
|
-
"/api/company-md/docs/{
|
|
1596
|
+
"/api/company-md/docs/{id}/sharing": {
|
|
1597
1597
|
parameters: {
|
|
1598
1598
|
query?: never;
|
|
1599
1599
|
header?: never;
|
|
@@ -1610,7 +1610,7 @@ export interface paths {
|
|
|
1610
1610
|
patch?: never;
|
|
1611
1611
|
trace?: never;
|
|
1612
1612
|
};
|
|
1613
|
-
"/api/company-md/docs/{
|
|
1613
|
+
"/api/company-md/docs/{id}/sharing/policy": {
|
|
1614
1614
|
parameters: {
|
|
1615
1615
|
query?: never;
|
|
1616
1616
|
header?: never;
|
|
@@ -1627,7 +1627,7 @@ export interface paths {
|
|
|
1627
1627
|
patch?: never;
|
|
1628
1628
|
trace?: never;
|
|
1629
1629
|
};
|
|
1630
|
-
"/api/company-md/docs/{
|
|
1630
|
+
"/api/company-md/docs/{id}/sharing/acl": {
|
|
1631
1631
|
parameters: {
|
|
1632
1632
|
query?: never;
|
|
1633
1633
|
header?: never;
|
|
@@ -1644,7 +1644,7 @@ export interface paths {
|
|
|
1644
1644
|
patch?: never;
|
|
1645
1645
|
trace?: never;
|
|
1646
1646
|
};
|
|
1647
|
-
"/api/company-md/docs/{
|
|
1647
|
+
"/api/company-md/docs/{id}/sharing/acl/{aclId}": {
|
|
1648
1648
|
parameters: {
|
|
1649
1649
|
query?: never;
|
|
1650
1650
|
header?: never;
|
|
@@ -4187,10 +4187,10 @@ export interface components {
|
|
|
4187
4187
|
};
|
|
4188
4188
|
CreateContextDocRequest: {
|
|
4189
4189
|
title: string;
|
|
4190
|
-
|
|
4190
|
+
parentDocId: string;
|
|
4191
4191
|
};
|
|
4192
4192
|
AssociateContextDocRequest: {
|
|
4193
|
-
|
|
4193
|
+
contextDocId: string;
|
|
4194
4194
|
};
|
|
4195
4195
|
TransferCompanyMdDocOwnerRequest: {
|
|
4196
4196
|
/** Format: uuid */
|
|
@@ -7421,7 +7421,7 @@ export interface operations {
|
|
|
7421
7421
|
query?: never;
|
|
7422
7422
|
header?: never;
|
|
7423
7423
|
path: {
|
|
7424
|
-
|
|
7424
|
+
id: string;
|
|
7425
7425
|
};
|
|
7426
7426
|
cookie?: never;
|
|
7427
7427
|
};
|
|
@@ -7443,7 +7443,7 @@ export interface operations {
|
|
|
7443
7443
|
query?: never;
|
|
7444
7444
|
header?: never;
|
|
7445
7445
|
path: {
|
|
7446
|
-
|
|
7446
|
+
id: string;
|
|
7447
7447
|
};
|
|
7448
7448
|
cookie?: never;
|
|
7449
7449
|
};
|
|
@@ -7467,7 +7467,7 @@ export interface operations {
|
|
|
7467
7467
|
query?: never;
|
|
7468
7468
|
header?: never;
|
|
7469
7469
|
path: {
|
|
7470
|
-
|
|
7470
|
+
id: string;
|
|
7471
7471
|
};
|
|
7472
7472
|
cookie?: never;
|
|
7473
7473
|
};
|
|
@@ -7515,7 +7515,7 @@ export interface operations {
|
|
|
7515
7515
|
query?: never;
|
|
7516
7516
|
header?: never;
|
|
7517
7517
|
path: {
|
|
7518
|
-
|
|
7518
|
+
id: string;
|
|
7519
7519
|
};
|
|
7520
7520
|
cookie?: never;
|
|
7521
7521
|
};
|
|
@@ -7559,7 +7559,7 @@ export interface operations {
|
|
|
7559
7559
|
query?: never;
|
|
7560
7560
|
header?: never;
|
|
7561
7561
|
path: {
|
|
7562
|
-
|
|
7562
|
+
id: string;
|
|
7563
7563
|
};
|
|
7564
7564
|
cookie?: never;
|
|
7565
7565
|
};
|
|
@@ -7583,8 +7583,8 @@ export interface operations {
|
|
|
7583
7583
|
query?: never;
|
|
7584
7584
|
header?: never;
|
|
7585
7585
|
path: {
|
|
7586
|
-
|
|
7587
|
-
|
|
7586
|
+
id: string;
|
|
7587
|
+
contextDocId: string;
|
|
7588
7588
|
};
|
|
7589
7589
|
cookie?: never;
|
|
7590
7590
|
};
|
|
@@ -7604,8 +7604,8 @@ export interface operations {
|
|
|
7604
7604
|
query?: never;
|
|
7605
7605
|
header?: never;
|
|
7606
7606
|
path: {
|
|
7607
|
-
slug: string;
|
|
7608
7607
|
id: string;
|
|
7608
|
+
contextDocId: string;
|
|
7609
7609
|
};
|
|
7610
7610
|
cookie?: never;
|
|
7611
7611
|
};
|
|
@@ -7639,7 +7639,7 @@ export interface operations {
|
|
|
7639
7639
|
query?: never;
|
|
7640
7640
|
header?: never;
|
|
7641
7641
|
path: {
|
|
7642
|
-
|
|
7642
|
+
id: string;
|
|
7643
7643
|
};
|
|
7644
7644
|
cookie?: never;
|
|
7645
7645
|
};
|
|
@@ -7679,7 +7679,7 @@ export interface operations {
|
|
|
7679
7679
|
query?: never;
|
|
7680
7680
|
header?: never;
|
|
7681
7681
|
path: {
|
|
7682
|
-
|
|
7682
|
+
id: string;
|
|
7683
7683
|
};
|
|
7684
7684
|
cookie?: never;
|
|
7685
7685
|
};
|
|
@@ -7692,7 +7692,7 @@ export interface operations {
|
|
|
7692
7692
|
};
|
|
7693
7693
|
content?: never;
|
|
7694
7694
|
};
|
|
7695
|
-
/** @description Retry limit exceeded or invalid
|
|
7695
|
+
/** @description Retry limit exceeded or invalid doc */
|
|
7696
7696
|
422: {
|
|
7697
7697
|
headers: {
|
|
7698
7698
|
[name: string]: unknown;
|
|
@@ -7706,7 +7706,7 @@ export interface operations {
|
|
|
7706
7706
|
query?: never;
|
|
7707
7707
|
header?: never;
|
|
7708
7708
|
path: {
|
|
7709
|
-
|
|
7709
|
+
id: string;
|
|
7710
7710
|
};
|
|
7711
7711
|
cookie?: never;
|
|
7712
7712
|
};
|
|
@@ -7824,7 +7824,7 @@ export interface operations {
|
|
|
7824
7824
|
query?: never;
|
|
7825
7825
|
header?: never;
|
|
7826
7826
|
path: {
|
|
7827
|
-
|
|
7827
|
+
id: string;
|
|
7828
7828
|
};
|
|
7829
7829
|
cookie?: never;
|
|
7830
7830
|
};
|
|
@@ -7846,7 +7846,7 @@ export interface operations {
|
|
|
7846
7846
|
query?: never;
|
|
7847
7847
|
header?: never;
|
|
7848
7848
|
path: {
|
|
7849
|
-
|
|
7849
|
+
id: string;
|
|
7850
7850
|
};
|
|
7851
7851
|
cookie?: never;
|
|
7852
7852
|
};
|
|
@@ -7870,7 +7870,7 @@ export interface operations {
|
|
|
7870
7870
|
query?: never;
|
|
7871
7871
|
header?: never;
|
|
7872
7872
|
path: {
|
|
7873
|
-
|
|
7873
|
+
id: string;
|
|
7874
7874
|
};
|
|
7875
7875
|
cookie?: never;
|
|
7876
7876
|
};
|
|
@@ -7894,7 +7894,7 @@ export interface operations {
|
|
|
7894
7894
|
query?: never;
|
|
7895
7895
|
header?: never;
|
|
7896
7896
|
path: {
|
|
7897
|
-
|
|
7897
|
+
id: string;
|
|
7898
7898
|
aclId: string;
|
|
7899
7899
|
};
|
|
7900
7900
|
cookie?: never;
|
|
@@ -7919,7 +7919,7 @@ export interface operations {
|
|
|
7919
7919
|
query?: never;
|
|
7920
7920
|
header?: never;
|
|
7921
7921
|
path: {
|
|
7922
|
-
|
|
7922
|
+
id: string;
|
|
7923
7923
|
aclId: string;
|
|
7924
7924
|
};
|
|
7925
7925
|
cookie?: never;
|
package/src/index.ts
CHANGED
|
@@ -408,6 +408,36 @@ export type {
|
|
|
408
408
|
export { SourceAuthoritySchema } from "./org/index";
|
|
409
409
|
export type { SourceAuthority } from "./org/index";
|
|
410
410
|
|
|
411
|
+
// Sync run summary: the outcome record of one adapter sync run (counts +
|
|
412
|
+
// cursor watermark). A flat report shape, not a structure fact. (ADR-CONT-085)
|
|
413
|
+
export { SyncRunSummarySchema } from "./org/index";
|
|
414
|
+
export type { SyncRunSummary } from "./org/index";
|
|
415
|
+
|
|
416
|
+
// Conflict record: one recorded reconciliation conflict surfaced during a sync.
|
|
417
|
+
// Operational audit record about a sync, not a structure fact. (ADR-CONT-085)
|
|
418
|
+
export { ConflictRecordSchema } from "./org/index";
|
|
419
|
+
export type { ConflictRecord } from "./org/index";
|
|
420
|
+
|
|
421
|
+
// Canonical facts: the adapter-seam ingestion batch — external-keyed Canonical*
|
|
422
|
+
// facts (each with provenance) plus a CanonicalFacts envelope. The reconciler
|
|
423
|
+
// consumes the batch into the internal org graph. (ADR-CONT-085 / ADR-CTRL-183)
|
|
424
|
+
export {
|
|
425
|
+
CanonicalPersonSchema,
|
|
426
|
+
CanonicalOrgUnitSchema,
|
|
427
|
+
CanonicalPositionSchema,
|
|
428
|
+
CanonicalOccupancySchema,
|
|
429
|
+
CanonicalReportingEdgeSchema,
|
|
430
|
+
CanonicalFactsSchema,
|
|
431
|
+
} from "./org/index";
|
|
432
|
+
export type {
|
|
433
|
+
CanonicalPerson,
|
|
434
|
+
CanonicalOrgUnit,
|
|
435
|
+
CanonicalPosition,
|
|
436
|
+
CanonicalOccupancy,
|
|
437
|
+
CanonicalReportingEdge,
|
|
438
|
+
CanonicalFacts,
|
|
439
|
+
} from "./org/index";
|
|
440
|
+
|
|
411
441
|
// Authority & Delegation vocabulary
|
|
412
442
|
export {
|
|
413
443
|
AuthoritySourceSchema,
|
package/src/org/README.md
CHANGED
|
@@ -40,6 +40,18 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
40
40
|
- `AuthoritySource` _(type)_
|
|
41
41
|
- `AuthoritySourceSchema` — Engine-internal authority source.
|
|
42
42
|
- `AuthorizableView` _(type)_ — Type for views that can be checked against VIEWSCOPEMAP.
|
|
43
|
+
- `CanonicalFacts` _(type)_
|
|
44
|
+
- `CanonicalFactsSchema` — The full normalized batch an external-source adapter emits in one pass: every person, org unit, position…
|
|
45
|
+
- `CanonicalOccupancy` _(type)_
|
|
46
|
+
- `CanonicalOccupancySchema` — An occupancy (a person holds a seat) as reported by an external source.
|
|
47
|
+
- `CanonicalOrgUnit` _(type)_
|
|
48
|
+
- `CanonicalOrgUnitSchema` — An org unit as reported by an external source.
|
|
49
|
+
- `CanonicalPerson` _(type)_
|
|
50
|
+
- `CanonicalPersonSchema` — A person as reported by an external source, keyed by external identity.
|
|
51
|
+
- `CanonicalPosition` _(type)_
|
|
52
|
+
- `CanonicalPositionSchema` — A position (seat) as reported by an external source, placed in a unit by external reference…
|
|
53
|
+
- `CanonicalReportingEdge` _(type)_
|
|
54
|
+
- `CanonicalReportingEdgeSchema` — A reporting edge (a seat reports to a manager seat) as reported by an external source.
|
|
43
55
|
- `CapabilitiesResponse` _(type)_
|
|
44
56
|
- `CapabilitiesResponseSchema` — The single validated capability surface the UI reads.
|
|
45
57
|
- `ChangeMemberRoleRequest` _(type)_ — Request payload for changing a member's role.
|
|
@@ -58,6 +70,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
58
70
|
- `CompanyMdSource` _(type)_
|
|
59
71
|
- `CompanyMdTreeNode` _(type)_
|
|
60
72
|
- `CompanyMdVisibility` _(type)_ — Visibility band for a Company.md document — the canonical AUTH `entity_visibility` vocabulary (, backend ). -…
|
|
73
|
+
- `ConflictRecord` _(type)_
|
|
74
|
+
- `ConflictRecordSchema`
|
|
61
75
|
- `CreateDelegationRequest` _(type)_
|
|
62
76
|
- `CreateDelegationRequestSchema`
|
|
63
77
|
- `CreateInviteRequest` _(type)_ — Request payload for creating an organization invite.
|
|
@@ -246,6 +260,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
246
260
|
- `SsoStepperStep` _(type)_ — Backend-authoritative stepper step.
|
|
247
261
|
- `SubmitInteractiveTaskResponse` _(type)_
|
|
248
262
|
- `SubmitInteractiveTaskResponseSchema` — Response for submitting a filled-in interactive chat task surface (e.g. the editable "change reporting…
|
|
263
|
+
- `SyncRunSummary` _(type)_
|
|
264
|
+
- `SyncRunSummarySchema`
|
|
249
265
|
- `TRANSFER_RESPONSIBILITIES` — Responsibility checklist items shown on the acceptance page.
|
|
250
266
|
- `TestSsoInitiation` _(type)_ — Initiation payload for a test SSO login attempt.
|
|
251
267
|
- `TestSsoInitiationDto` _(type)_ — Initiation payload for a test SSO login attempt.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CanonicalPersonSchema,
|
|
5
|
+
CanonicalOrgUnitSchema,
|
|
6
|
+
CanonicalPositionSchema,
|
|
7
|
+
CanonicalOccupancySchema,
|
|
8
|
+
CanonicalReportingEdgeSchema,
|
|
9
|
+
CanonicalFactsSchema,
|
|
10
|
+
} from "../canonical-facts.js";
|
|
11
|
+
|
|
12
|
+
const provenance = {
|
|
13
|
+
tier: "import",
|
|
14
|
+
source: "bamboohr",
|
|
15
|
+
confidence: 0.9,
|
|
16
|
+
locked: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("CanonicalPersonSchema", () => {
|
|
20
|
+
const base = {
|
|
21
|
+
externalSourceId: "emp-123",
|
|
22
|
+
externalSourceSystem: "bamboohr",
|
|
23
|
+
displayName: "Ada Lovelace",
|
|
24
|
+
provenance,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it("parses a minimal person without an email", () => {
|
|
28
|
+
const p = CanonicalPersonSchema.parse(base);
|
|
29
|
+
expect(p.externalSourceId).toBe("emp-123");
|
|
30
|
+
expect(p.primaryEmail).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("accepts a null or valid email", () => {
|
|
34
|
+
expect(
|
|
35
|
+
CanonicalPersonSchema.parse({ ...base, primaryEmail: null }).primaryEmail,
|
|
36
|
+
).toBeNull();
|
|
37
|
+
expect(
|
|
38
|
+
CanonicalPersonSchema.parse({ ...base, primaryEmail: "ada@x.io" })
|
|
39
|
+
.primaryEmail,
|
|
40
|
+
).toBe("ada@x.io");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rejects a malformed email", () => {
|
|
44
|
+
expect(() =>
|
|
45
|
+
CanonicalPersonSchema.parse({ ...base, primaryEmail: "not-an-email" }),
|
|
46
|
+
).toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("rejects empty external identity (open strings still require content)", () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
CanonicalPersonSchema.parse({ ...base, externalSourceId: "" }),
|
|
52
|
+
).toThrow();
|
|
53
|
+
expect(() =>
|
|
54
|
+
CanonicalPersonSchema.parse({ ...base, externalSourceSystem: "" }),
|
|
55
|
+
).toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("requires a provenance envelope (canonical facts are inputs)", () => {
|
|
59
|
+
const { provenance: _omitted, ...without } = base;
|
|
60
|
+
expect(() => CanonicalPersonSchema.parse(without)).toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("does NOT require a uuid for external ids", () => {
|
|
64
|
+
// External ids are arbitrary strings from the origin system, not uuids.
|
|
65
|
+
expect(() =>
|
|
66
|
+
CanonicalPersonSchema.parse({ ...base, externalSourceId: "abc/42" }),
|
|
67
|
+
).not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("CanonicalOrgUnitSchema", () => {
|
|
72
|
+
const base = {
|
|
73
|
+
externalSourceId: "dept-9",
|
|
74
|
+
externalSourceSystem: "workday",
|
|
75
|
+
name: "Platform",
|
|
76
|
+
provenance,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
it("parses a root unit (no parent)", () => {
|
|
80
|
+
const u = CanonicalOrgUnitSchema.parse(base);
|
|
81
|
+
expect(u.parentExternalSourceId).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("accepts a parent external reference or null", () => {
|
|
85
|
+
expect(
|
|
86
|
+
CanonicalOrgUnitSchema.parse({
|
|
87
|
+
...base,
|
|
88
|
+
parentExternalSourceId: "dept-1",
|
|
89
|
+
}).parentExternalSourceId,
|
|
90
|
+
).toBe("dept-1");
|
|
91
|
+
expect(
|
|
92
|
+
CanonicalOrgUnitSchema.parse({ ...base, parentExternalSourceId: null })
|
|
93
|
+
.parentExternalSourceId,
|
|
94
|
+
).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("rejects an empty name", () => {
|
|
98
|
+
expect(() => CanonicalOrgUnitSchema.parse({ ...base, name: "" })).toThrow();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("CanonicalPositionSchema", () => {
|
|
103
|
+
const base = {
|
|
104
|
+
externalSourceId: "pos-7",
|
|
105
|
+
externalSourceSystem: "workday",
|
|
106
|
+
title: "Staff Engineer",
|
|
107
|
+
unitExternalSourceId: "dept-9",
|
|
108
|
+
provenance,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
it("parses a seat placed in a unit by external reference", () => {
|
|
112
|
+
const p = CanonicalPositionSchema.parse(base);
|
|
113
|
+
expect(p.unitExternalSourceId).toBe("dept-9");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects an empty title or missing unit reference", () => {
|
|
117
|
+
expect(() =>
|
|
118
|
+
CanonicalPositionSchema.parse({ ...base, title: "" }),
|
|
119
|
+
).toThrow();
|
|
120
|
+
const { unitExternalSourceId: _u, ...without } = base;
|
|
121
|
+
expect(() => CanonicalPositionSchema.parse(without)).toThrow();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("CanonicalOccupancySchema", () => {
|
|
126
|
+
const base = {
|
|
127
|
+
externalSourceId: "occ-3",
|
|
128
|
+
externalSourceSystem: "bamboohr",
|
|
129
|
+
positionExternalSourceId: "pos-7",
|
|
130
|
+
personExternalSourceId: "emp-123",
|
|
131
|
+
provenance,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
it("parses a person-holds-seat fact keyed by external ids", () => {
|
|
135
|
+
const o = CanonicalOccupancySchema.parse(base);
|
|
136
|
+
expect(o.positionExternalSourceId).toBe("pos-7");
|
|
137
|
+
expect(o.personExternalSourceId).toBe("emp-123");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("requires both sides of the relation", () => {
|
|
141
|
+
const { positionExternalSourceId: _p, ...noPosition } = base;
|
|
142
|
+
expect(() => CanonicalOccupancySchema.parse(noPosition)).toThrow();
|
|
143
|
+
const { personExternalSourceId: _q, ...noPerson } = base;
|
|
144
|
+
expect(() => CanonicalOccupancySchema.parse(noPerson)).toThrow();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("CanonicalReportingEdgeSchema", () => {
|
|
149
|
+
const base = {
|
|
150
|
+
externalSourceId: "edge-2",
|
|
151
|
+
externalSourceSystem: "workday",
|
|
152
|
+
reportPositionExternalSourceId: "pos-7",
|
|
153
|
+
managerPositionExternalSourceId: "pos-1",
|
|
154
|
+
relationshipType: "solid",
|
|
155
|
+
provenance,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
it("parses a seat -> manager-seat edge", () => {
|
|
159
|
+
const e = CanonicalReportingEdgeSchema.parse(base);
|
|
160
|
+
expect(e.relationshipType).toBe("solid");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("accepts the dotted relationship type", () => {
|
|
164
|
+
expect(
|
|
165
|
+
CanonicalReportingEdgeSchema.parse({
|
|
166
|
+
...base,
|
|
167
|
+
relationshipType: "dotted",
|
|
168
|
+
}).relationshipType,
|
|
169
|
+
).toBe("dotted");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("rejects an unknown relationship type", () => {
|
|
173
|
+
expect(() =>
|
|
174
|
+
CanonicalReportingEdgeSchema.parse({
|
|
175
|
+
...base,
|
|
176
|
+
relationshipType: "matrix",
|
|
177
|
+
}),
|
|
178
|
+
).toThrow();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("CanonicalFactsSchema", () => {
|
|
183
|
+
it("parses an empty batch (all arrays empty)", () => {
|
|
184
|
+
const facts = CanonicalFactsSchema.parse({
|
|
185
|
+
people: [],
|
|
186
|
+
orgUnits: [],
|
|
187
|
+
positions: [],
|
|
188
|
+
occupancies: [],
|
|
189
|
+
reportingEdges: [],
|
|
190
|
+
});
|
|
191
|
+
expect(facts.people).toEqual([]);
|
|
192
|
+
expect(facts.reportingEdges).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("parses a populated batch with one of each fact", () => {
|
|
196
|
+
const facts = CanonicalFactsSchema.parse({
|
|
197
|
+
people: [
|
|
198
|
+
{
|
|
199
|
+
externalSourceId: "emp-123",
|
|
200
|
+
externalSourceSystem: "bamboohr",
|
|
201
|
+
displayName: "Ada Lovelace",
|
|
202
|
+
provenance,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
orgUnits: [
|
|
206
|
+
{
|
|
207
|
+
externalSourceId: "dept-9",
|
|
208
|
+
externalSourceSystem: "workday",
|
|
209
|
+
name: "Platform",
|
|
210
|
+
provenance,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
positions: [
|
|
214
|
+
{
|
|
215
|
+
externalSourceId: "pos-7",
|
|
216
|
+
externalSourceSystem: "workday",
|
|
217
|
+
title: "Staff Engineer",
|
|
218
|
+
unitExternalSourceId: "dept-9",
|
|
219
|
+
provenance,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
occupancies: [
|
|
223
|
+
{
|
|
224
|
+
externalSourceId: "occ-3",
|
|
225
|
+
externalSourceSystem: "bamboohr",
|
|
226
|
+
positionExternalSourceId: "pos-7",
|
|
227
|
+
personExternalSourceId: "emp-123",
|
|
228
|
+
provenance,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
reportingEdges: [
|
|
232
|
+
{
|
|
233
|
+
externalSourceId: "edge-2",
|
|
234
|
+
externalSourceSystem: "workday",
|
|
235
|
+
reportPositionExternalSourceId: "pos-7",
|
|
236
|
+
managerPositionExternalSourceId: "pos-1",
|
|
237
|
+
relationshipType: "solid",
|
|
238
|
+
provenance,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
expect(facts.people).toHaveLength(1);
|
|
243
|
+
expect(facts.positions[0].unitExternalSourceId).toBe("dept-9");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("requires every array key to be present", () => {
|
|
247
|
+
expect(() =>
|
|
248
|
+
CanonicalFactsSchema.parse({ people: [], orgUnits: [], positions: [] }),
|
|
249
|
+
).toThrow();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("rejects a member that fails its element schema", () => {
|
|
253
|
+
expect(() =>
|
|
254
|
+
CanonicalFactsSchema.parse({
|
|
255
|
+
people: [
|
|
256
|
+
{
|
|
257
|
+
externalSourceId: "",
|
|
258
|
+
externalSourceSystem: "x",
|
|
259
|
+
displayName: "y",
|
|
260
|
+
provenance,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
orgUnits: [],
|
|
264
|
+
positions: [],
|
|
265
|
+
occupancies: [],
|
|
266
|
+
reportingEdges: [],
|
|
267
|
+
}),
|
|
268
|
+
).toThrow();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { ConflictRecordSchema } from "../conflict-record.js";
|
|
4
|
+
import { FactSourceTierSchema } from "../structure-facts.js";
|
|
5
|
+
|
|
6
|
+
describe("ConflictRecordSchema", () => {
|
|
7
|
+
const base = {
|
|
8
|
+
id: "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
|
9
|
+
runId: "9c5b94b1-35ad-49bb-b118-8e8fc24abf80",
|
|
10
|
+
entityType: "person",
|
|
11
|
+
entityId: "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
|
|
12
|
+
field: "managerPositionId",
|
|
13
|
+
importedValue: "seat-a",
|
|
14
|
+
currentValue: "seat-b",
|
|
15
|
+
winningValue: "seat-b",
|
|
16
|
+
winningTier: "user",
|
|
17
|
+
reason: "Human override outranks the imported feed.",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
it("parses a valid conflict record", () => {
|
|
21
|
+
const record = ConflictRecordSchema.parse(base);
|
|
22
|
+
expect(record.field).toBe("managerPositionId");
|
|
23
|
+
expect(record.winningTier).toBe("user");
|
|
24
|
+
expect(record.winningValue).toBe("seat-b");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("accepts any entity kind and field as open strings", () => {
|
|
28
|
+
for (const entityType of ["person", "position", "occupancy", "future"]) {
|
|
29
|
+
expect(
|
|
30
|
+
ConflictRecordSchema.parse({ ...base, entityType }).entityType,
|
|
31
|
+
).toBe(entityType);
|
|
32
|
+
}
|
|
33
|
+
expect(
|
|
34
|
+
ConflictRecordSchema.parse({ ...base, field: "anything_at_all" }).field,
|
|
35
|
+
).toBe("anything_at_all");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("accepts every FactSourceTier as winningTier", () => {
|
|
39
|
+
for (const tier of FactSourceTierSchema.options) {
|
|
40
|
+
expect(
|
|
41
|
+
ConflictRecordSchema.parse({ ...base, winningTier: tier }).winningTier,
|
|
42
|
+
).toBe(tier);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects an unknown winningTier", () => {
|
|
47
|
+
expect(() =>
|
|
48
|
+
ConflictRecordSchema.parse({ ...base, winningTier: "guess" }),
|
|
49
|
+
).toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("allows null competing values", () => {
|
|
53
|
+
const record = ConflictRecordSchema.parse({
|
|
54
|
+
...base,
|
|
55
|
+
importedValue: null,
|
|
56
|
+
currentValue: null,
|
|
57
|
+
winningValue: null,
|
|
58
|
+
});
|
|
59
|
+
expect(record.importedValue).toBeNull();
|
|
60
|
+
expect(record.currentValue).toBeNull();
|
|
61
|
+
expect(record.winningValue).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("treats reason as optional and nullable", () => {
|
|
65
|
+
const { reason: _reason, ...withoutReason } = base;
|
|
66
|
+
expect(() => ConflictRecordSchema.parse(withoutReason)).not.toThrow();
|
|
67
|
+
expect(
|
|
68
|
+
ConflictRecordSchema.parse({ ...base, reason: null }).reason,
|
|
69
|
+
).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects a reason longer than 2000 chars", () => {
|
|
73
|
+
expect(() =>
|
|
74
|
+
ConflictRecordSchema.parse({ ...base, reason: "x".repeat(2001) }),
|
|
75
|
+
).toThrow();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("rejects a non-uuid id, runId, or entityId", () => {
|
|
79
|
+
expect(() => ConflictRecordSchema.parse({ ...base, id: "nope" })).toThrow();
|
|
80
|
+
expect(() =>
|
|
81
|
+
ConflictRecordSchema.parse({ ...base, runId: "nope" }),
|
|
82
|
+
).toThrow();
|
|
83
|
+
expect(() =>
|
|
84
|
+
ConflictRecordSchema.parse({ ...base, entityId: "nope" }),
|
|
85
|
+
).toThrow();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rejects an empty entityType or field", () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
ConflictRecordSchema.parse({ ...base, entityType: "" }),
|
|
91
|
+
).toThrow();
|
|
92
|
+
expect(() => ConflictRecordSchema.parse({ ...base, field: "" })).toThrow();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { SyncRunSummarySchema } from "../sync-run.js";
|
|
4
|
+
|
|
5
|
+
describe("SyncRunSummarySchema", () => {
|
|
6
|
+
const base = {
|
|
7
|
+
id: "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
|
8
|
+
startedAt: "2026-01-01T00:00:00.000Z",
|
|
9
|
+
processed: 100,
|
|
10
|
+
changed: 12,
|
|
11
|
+
skipped: 85,
|
|
12
|
+
conflicts: 2,
|
|
13
|
+
failures: 1,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
it("parses a minimal in-progress run without endedAt or sinceCursor", () => {
|
|
17
|
+
const run = SyncRunSummarySchema.parse(base);
|
|
18
|
+
expect(run.processed).toBe(100);
|
|
19
|
+
expect(run.endedAt).toBeUndefined();
|
|
20
|
+
expect(run.sinceCursor).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("accepts null and ISO datetime for endedAt", () => {
|
|
24
|
+
expect(
|
|
25
|
+
SyncRunSummarySchema.parse({ ...base, endedAt: null }).endedAt,
|
|
26
|
+
).toBeNull();
|
|
27
|
+
const ended = SyncRunSummarySchema.parse({
|
|
28
|
+
...base,
|
|
29
|
+
endedAt: "2026-01-01T00:05:00.000Z",
|
|
30
|
+
});
|
|
31
|
+
expect(ended.endedAt).toBe("2026-01-01T00:05:00.000Z");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("accepts null and a string watermark for sinceCursor", () => {
|
|
35
|
+
expect(
|
|
36
|
+
SyncRunSummarySchema.parse({ ...base, sinceCursor: null }).sinceCursor,
|
|
37
|
+
).toBeNull();
|
|
38
|
+
expect(
|
|
39
|
+
SyncRunSummarySchema.parse({
|
|
40
|
+
...base,
|
|
41
|
+
sinceCursor: "2026-01-01T00:00:00Z",
|
|
42
|
+
}).sinceCursor,
|
|
43
|
+
).toBe("2026-01-01T00:00:00Z");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects a non-ISO startedAt", () => {
|
|
47
|
+
expect(() =>
|
|
48
|
+
SyncRunSummarySchema.parse({ ...base, startedAt: "2026-01-01" }),
|
|
49
|
+
).toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects a non-uuid id", () => {
|
|
53
|
+
expect(() => SyncRunSummarySchema.parse({ ...base, id: "nope" })).toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects negative or non-integer counters", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
SyncRunSummarySchema.parse({ ...base, processed: -1 }),
|
|
59
|
+
).toThrow();
|
|
60
|
+
expect(() =>
|
|
61
|
+
SyncRunSummarySchema.parse({ ...base, changed: 1.5 }),
|
|
62
|
+
).toThrow();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("requires startedAt (run has begun)", () => {
|
|
66
|
+
const { startedAt: _omitted, ...withoutStartedAt } = base;
|
|
67
|
+
expect(() => SyncRunSummarySchema.parse(withoutStartedAt)).toThrow();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Facts — the adapter-seam ingestion vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* A `CanonicalFacts` batch is the NORMALIZED shape an external-source adapter
|
|
5
|
+
* (an HRIS sync, a CSV import, an org-chart scrape) emits BEFORE reconciliation
|
|
6
|
+
* into the internal org graph. It is the single hand-off point between "what a
|
|
7
|
+
* source said" and "what the org graph becomes": adapters translate their
|
|
8
|
+
* proprietary payloads into this vocabulary, and the reconciler consumes it to
|
|
9
|
+
* create/supersede internal Person / OrgUnit / Position / Occupancy / reporting
|
|
10
|
+
* records. See contracts ADR-CONT-085 and control ADR-CTRL-183.
|
|
11
|
+
*
|
|
12
|
+
* Two properties make these facts distinct from the in-graph schemas they mirror
|
|
13
|
+
* ({@link PersonSchema}, {@link PositionSchema}, {@link OccupancySchema}, …):
|
|
14
|
+
*
|
|
15
|
+
* 1. They are keyed by EXTERNAL identity, not internal uuids. Every fact carries
|
|
16
|
+
* `externalSourceId` (the id in the origin system, an arbitrary string — NOT
|
|
17
|
+
* a uuid) plus `externalSourceSystem` (an OPEN string naming the system, e.g.
|
|
18
|
+
* `bamboohr` | `workday` | `csv`). Cross-references between facts in a batch
|
|
19
|
+
* (an occupancy's person, a reporting edge's manager seat) are expressed with
|
|
20
|
+
* those external ids too, because internal uuids do not exist yet at ingest.
|
|
21
|
+
*
|
|
22
|
+
* 2. They ALL carry a {@link FactProvenanceSchema} envelope — including
|
|
23
|
+
* `CanonicalPerson`. This is a deliberate divergence from the in-graph
|
|
24
|
+
* vocabulary, where identity types (`Person`, `SourceAuthority`) carry no
|
|
25
|
+
* provenance: a canonical fact is by definition a reconciliation INPUT, and
|
|
26
|
+
* the truth hierarchy (user > sync > import > inferred) plus the
|
|
27
|
+
* append+supersede correction model must apply uniformly to every fact a
|
|
28
|
+
* source reports, identity-shaped or not.
|
|
29
|
+
*
|
|
30
|
+
* `externalSourceSystem` mirrors the open-string idiom of `provenance.source`
|
|
31
|
+
* and `SourceAuthority.systemOfRecord`: new origin systems need no schema change.
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
|
|
35
|
+
import { FactProvenanceSchema } from "./structure-facts";
|
|
36
|
+
import { PositionReportingRelationshipTypeSchema } from "./position-reporting";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// CanonicalPerson — a normalized person reported by an external source
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A person as reported by an external source, keyed by external identity. Mirrors
|
|
44
|
+
* {@link PersonSchema} but carries provenance because it is a reconciliation
|
|
45
|
+
* input (see file header for why identity-shaped canonical facts still carry it).
|
|
46
|
+
*/
|
|
47
|
+
export const CanonicalPersonSchema = z.object({
|
|
48
|
+
/** Id of this person in the origin system — arbitrary string, NOT a uuid. */
|
|
49
|
+
externalSourceId: z.string().min(1),
|
|
50
|
+
/** Origin system naming this id — OPEN string (e.g. `bamboohr`, `workday`). */
|
|
51
|
+
externalSourceSystem: z.string().min(1),
|
|
52
|
+
/** Human-facing display name as reported by the source. */
|
|
53
|
+
displayName: z.string().min(1),
|
|
54
|
+
/** Primary email as reported; `null`/absent when the source omits it. */
|
|
55
|
+
primaryEmail: z.string().email().nullable().optional(),
|
|
56
|
+
/** Provenance of this fact (truth hierarchy + supersede model). */
|
|
57
|
+
provenance: FactProvenanceSchema,
|
|
58
|
+
});
|
|
59
|
+
export type CanonicalPerson = z.infer<typeof CanonicalPersonSchema>;
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// CanonicalOrgUnit — a normalized org unit reported by an external source
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* An org unit as reported by an external source. The parent reference is an
|
|
67
|
+
* external id (`parentExternalSourceId`), so the unit tree is reconstructable
|
|
68
|
+
* within a batch before any internal uuid exists; a root unit has no parent.
|
|
69
|
+
*/
|
|
70
|
+
export const CanonicalOrgUnitSchema = z.object({
|
|
71
|
+
/** Id of this unit in the origin system — arbitrary string, NOT a uuid. */
|
|
72
|
+
externalSourceId: z.string().min(1),
|
|
73
|
+
/** Origin system naming this id — OPEN string. */
|
|
74
|
+
externalSourceSystem: z.string().min(1),
|
|
75
|
+
/** Human-facing unit name as reported by the source. */
|
|
76
|
+
name: z.string().min(1),
|
|
77
|
+
/** External id of the parent unit; `null`/absent for a root unit. */
|
|
78
|
+
parentExternalSourceId: z.string().min(1).nullable().optional(),
|
|
79
|
+
/** Provenance of this fact (truth hierarchy + supersede model). */
|
|
80
|
+
provenance: FactProvenanceSchema,
|
|
81
|
+
});
|
|
82
|
+
export type CanonicalOrgUnit = z.infer<typeof CanonicalOrgUnitSchema>;
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// CanonicalPosition — a normalized seat reported by an external source
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A position (seat) as reported by an external source, placed in a unit by
|
|
90
|
+
* external reference (`unitExternalSourceId`). Mirrors {@link PositionSchema};
|
|
91
|
+
* `title` is the seat's LABEL, never the occupant's identity.
|
|
92
|
+
*/
|
|
93
|
+
export const CanonicalPositionSchema = z.object({
|
|
94
|
+
/** Id of this seat in the origin system — arbitrary string, NOT a uuid. */
|
|
95
|
+
externalSourceId: z.string().min(1),
|
|
96
|
+
/** Origin system naming this id — OPEN string. */
|
|
97
|
+
externalSourceSystem: z.string().min(1),
|
|
98
|
+
/** Human-facing label for the seat, NOT the occupant's identity. */
|
|
99
|
+
title: z.string().min(1),
|
|
100
|
+
/** External id of the org unit this seat lives in. */
|
|
101
|
+
unitExternalSourceId: z.string().min(1),
|
|
102
|
+
/** Provenance of this fact (truth hierarchy + supersede model). */
|
|
103
|
+
provenance: FactProvenanceSchema,
|
|
104
|
+
});
|
|
105
|
+
export type CanonicalPosition = z.infer<typeof CanonicalPositionSchema>;
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// CanonicalOccupancy — a normalized holds_position fact (external-keyed)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* An occupancy (a person holds a seat) as reported by an external source. Both
|
|
113
|
+
* sides are external references: `positionExternalSourceId` and
|
|
114
|
+
* `personExternalSourceId`. Mirrors {@link OccupancySchema} at the ingestion
|
|
115
|
+
* seam.
|
|
116
|
+
*/
|
|
117
|
+
export const CanonicalOccupancySchema = z.object({
|
|
118
|
+
/** Id of this occupancy in the origin system — arbitrary string, NOT a uuid. */
|
|
119
|
+
externalSourceId: z.string().min(1),
|
|
120
|
+
/** Origin system naming this id — OPEN string. */
|
|
121
|
+
externalSourceSystem: z.string().min(1),
|
|
122
|
+
/** External id of the position (seat) being held. */
|
|
123
|
+
positionExternalSourceId: z.string().min(1),
|
|
124
|
+
/** External id of the person holding the seat. */
|
|
125
|
+
personExternalSourceId: z.string().min(1),
|
|
126
|
+
/** Provenance of this fact (truth hierarchy + supersede model). */
|
|
127
|
+
provenance: FactProvenanceSchema,
|
|
128
|
+
});
|
|
129
|
+
export type CanonicalOccupancy = z.infer<typeof CanonicalOccupancySchema>;
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// CanonicalReportingEdge — a normalized seat -> manager-seat reporting edge
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A reporting edge (a seat reports to a manager seat) as reported by an external
|
|
137
|
+
* source. Both seats are external references; `relationshipType` reuses the
|
|
138
|
+
* org-domain {@link PositionReportingRelationshipTypeSchema} (solid vs dotted).
|
|
139
|
+
* Mirrors {@link PositionReportingSchema} at the ingestion seam.
|
|
140
|
+
*/
|
|
141
|
+
export const CanonicalReportingEdgeSchema = z.object({
|
|
142
|
+
/** Id of this edge in the origin system — arbitrary string, NOT a uuid. */
|
|
143
|
+
externalSourceId: z.string().min(1),
|
|
144
|
+
/** Origin system naming this id — OPEN string. */
|
|
145
|
+
externalSourceSystem: z.string().min(1),
|
|
146
|
+
/** External id of the reporting (subordinate) seat. */
|
|
147
|
+
reportPositionExternalSourceId: z.string().min(1),
|
|
148
|
+
/** External id of the manager seat this seat reports to. */
|
|
149
|
+
managerPositionExternalSourceId: z.string().min(1),
|
|
150
|
+
/** Solid (primary) vs dotted (matrixed) reporting line. */
|
|
151
|
+
relationshipType: PositionReportingRelationshipTypeSchema,
|
|
152
|
+
/** Provenance of this fact (truth hierarchy + supersede model). */
|
|
153
|
+
provenance: FactProvenanceSchema,
|
|
154
|
+
});
|
|
155
|
+
export type CanonicalReportingEdge = z.infer<
|
|
156
|
+
typeof CanonicalReportingEdgeSchema
|
|
157
|
+
>;
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// CanonicalFacts — the batch envelope an adapter emits for reconciliation
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The full normalized batch an external-source adapter emits in one pass: every
|
|
165
|
+
* person, org unit, position, occupancy, and reporting edge it observed, each
|
|
166
|
+
* carrying external identity + provenance. The reconciler consumes this whole
|
|
167
|
+
* envelope to create/supersede internal org-graph records.
|
|
168
|
+
*/
|
|
169
|
+
export const CanonicalFactsSchema = z.object({
|
|
170
|
+
/** Every person observed in this batch. */
|
|
171
|
+
people: z.array(CanonicalPersonSchema),
|
|
172
|
+
/** Every org unit observed in this batch. */
|
|
173
|
+
orgUnits: z.array(CanonicalOrgUnitSchema),
|
|
174
|
+
/** Every position (seat) observed in this batch. */
|
|
175
|
+
positions: z.array(CanonicalPositionSchema),
|
|
176
|
+
/** Every occupancy (person-holds-seat) observed in this batch. */
|
|
177
|
+
occupancies: z.array(CanonicalOccupancySchema),
|
|
178
|
+
/** Every seat-to-manager-seat reporting edge observed in this batch. */
|
|
179
|
+
reportingEdges: z.array(CanonicalReportingEdgeSchema),
|
|
180
|
+
});
|
|
181
|
+
export type CanonicalFacts = z.infer<typeof CanonicalFactsSchema>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConflictRecord Vocabulary — a single recorded reconciliation conflict.
|
|
3
|
+
*
|
|
4
|
+
* When a connector's normalized `CanonicalFacts` are reconciled against the
|
|
5
|
+
* org graph's existing facts, two systems may disagree — for example, two HR
|
|
6
|
+
* feeds reporting a different manager for the same person, or a feed
|
|
7
|
+
* contradicting a human override. A ConflictRecord is the audit trail for one
|
|
8
|
+
* such disagreement: it names the entity and field in conflict, the competing
|
|
9
|
+
* values (the value the feed imported vs. the value already held), the value
|
|
10
|
+
* that won, the tier of the winning value, and a human-meaningful reason. It is
|
|
11
|
+
* the record an operator and the override UX read to understand what happened
|
|
12
|
+
* during a sync (ADR-CONT-085, control ADR-CTRL-183).
|
|
13
|
+
*
|
|
14
|
+
* ConflictRecord is an operational record ABOUT a sync, not a structure fact:
|
|
15
|
+
* it references the precedence axis ({@link FactSourceTierSchema}) rather than
|
|
16
|
+
* carrying a full {@link FactProvenance} envelope, mirroring how SourceAuthority
|
|
17
|
+
* and SyncRunSummary stay distinct from the provenance-bearing facts. Like the
|
|
18
|
+
* sibling org schemas, `entityType` and `field` are OPEN strings (no enum) so
|
|
19
|
+
* new entity kinds and field names need no schema change, and `entityId`/`runId`
|
|
20
|
+
* are uuids. `winningTier` REUSES the closed `FactSourceTier` precedence axis
|
|
21
|
+
* rather than redefining it. The competing values are serialized as strings and
|
|
22
|
+
* are nullable because a conflicting (or absent) side may hold no value.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
import { FactSourceTierSchema } from "./structure-facts";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// ConflictRecord — one recorded conflict surfaced while reconciling a sync
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const ConflictRecordSchema = z.object({
|
|
33
|
+
/** Identifier of this conflict record. */
|
|
34
|
+
id: z.string().uuid(),
|
|
35
|
+
/** The sync run during which this conflict was surfaced. */
|
|
36
|
+
runId: z.string().uuid(),
|
|
37
|
+
/**
|
|
38
|
+
* Kind of org-graph entity the conflicting field belongs to (e.g. "person",
|
|
39
|
+
* "position", "occupancy"). An OPEN string so new entity kinds need no schema
|
|
40
|
+
* change.
|
|
41
|
+
*/
|
|
42
|
+
entityType: z.string().min(1),
|
|
43
|
+
/** Identifier of the specific entity instance in conflict. */
|
|
44
|
+
entityId: z.string().uuid(),
|
|
45
|
+
/** Name of the single field that was in conflict. */
|
|
46
|
+
field: z.string().min(1),
|
|
47
|
+
/**
|
|
48
|
+
* The value the connector's `CanonicalFacts` reported for this field,
|
|
49
|
+
* serialized as a string. `null` when the imported side carried no value.
|
|
50
|
+
*/
|
|
51
|
+
importedValue: z.string().nullable(),
|
|
52
|
+
/**
|
|
53
|
+
* The value already held in the org graph for this field before
|
|
54
|
+
* reconciliation, serialized as a string. `null` when no prior value existed.
|
|
55
|
+
*/
|
|
56
|
+
currentValue: z.string().nullable(),
|
|
57
|
+
/**
|
|
58
|
+
* The value that won reconciliation, serialized as a string. `null` when the
|
|
59
|
+
* resolution cleared the field.
|
|
60
|
+
*/
|
|
61
|
+
winningValue: z.string().nullable(),
|
|
62
|
+
/**
|
|
63
|
+
* The precedence tier of the value that won — the same closed truth-hierarchy
|
|
64
|
+
* axis used across the org model ({@link FactSourceTierSchema}). Reused, not
|
|
65
|
+
* redefined.
|
|
66
|
+
*/
|
|
67
|
+
winningTier: FactSourceTierSchema,
|
|
68
|
+
/**
|
|
69
|
+
* Human-meaningful WHY the conflict resolved as it did — a free-text audit
|
|
70
|
+
* record of intent, capped at 2000 chars to mirror the `FactProvenance.reason`
|
|
71
|
+
* / unit-owner reason API cap. `null`/absent when no narrative was recorded.
|
|
72
|
+
*/
|
|
73
|
+
reason: z.string().max(2000).nullable().optional(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export type ConflictRecord = z.infer<typeof ConflictRecordSchema>;
|
package/src/org/index.ts
CHANGED
|
@@ -126,6 +126,36 @@ export type {
|
|
|
126
126
|
export { SourceAuthoritySchema } from "./source-authority";
|
|
127
127
|
export type { SourceAuthority } from "./source-authority";
|
|
128
128
|
|
|
129
|
+
// Sync run summary: the outcome record of one adapter sync run (counts +
|
|
130
|
+
// cursor watermark). A flat report shape, not a structure fact. (ADR-CONT-085)
|
|
131
|
+
export { SyncRunSummarySchema } from "./sync-run";
|
|
132
|
+
export type { SyncRunSummary } from "./sync-run";
|
|
133
|
+
|
|
134
|
+
// Conflict record: one recorded reconciliation conflict surfaced during a sync.
|
|
135
|
+
// Operational audit record about a sync, not a structure fact. (ADR-CONT-085)
|
|
136
|
+
export { ConflictRecordSchema } from "./conflict-record";
|
|
137
|
+
export type { ConflictRecord } from "./conflict-record";
|
|
138
|
+
|
|
139
|
+
// Canonical facts: the adapter-seam ingestion batch. Each fact is keyed by
|
|
140
|
+
// external identity (externalSourceId/externalSourceSystem) and carries a
|
|
141
|
+
// FactProvenance envelope; the reconciler consumes the batch. (ADR-CONT-085 / ADR-CTRL-183)
|
|
142
|
+
export {
|
|
143
|
+
CanonicalPersonSchema,
|
|
144
|
+
CanonicalOrgUnitSchema,
|
|
145
|
+
CanonicalPositionSchema,
|
|
146
|
+
CanonicalOccupancySchema,
|
|
147
|
+
CanonicalReportingEdgeSchema,
|
|
148
|
+
CanonicalFactsSchema,
|
|
149
|
+
} from "./canonical-facts";
|
|
150
|
+
export type {
|
|
151
|
+
CanonicalPerson,
|
|
152
|
+
CanonicalOrgUnit,
|
|
153
|
+
CanonicalPosition,
|
|
154
|
+
CanonicalOccupancy,
|
|
155
|
+
CanonicalReportingEdge,
|
|
156
|
+
CanonicalFacts,
|
|
157
|
+
} from "./canonical-facts";
|
|
158
|
+
|
|
129
159
|
// Canonical OrgUnit tree ordering (PRD-00506)
|
|
130
160
|
export type { TreeOrderableNode } from "./tree-ordering";
|
|
131
161
|
export { orderTreeNodes } from "./tree-ordering";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncRunSummary: the outcome record of a single adapter sync run.
|
|
3
|
+
*
|
|
4
|
+
* An external source adapter (BambooHR, Workday, CSV import, ...) ingests
|
|
5
|
+
* {@link CanonicalFacts} into the org graph in discrete RUNS. A SyncRunSummary
|
|
6
|
+
* is the audit/observability record of one such run: how much was processed,
|
|
7
|
+
* how much actually changed, and how far the run advanced its incremental
|
|
8
|
+
* cursor. It is a flat report shape — NOT a structure fact — so it carries no
|
|
9
|
+
* {@link FactProvenanceSchema} envelope; provenance lives on the individual
|
|
10
|
+
* facts a run produces, not on the run summary itself.
|
|
11
|
+
*
|
|
12
|
+
* The counters are independent tallies of what the run did:
|
|
13
|
+
*
|
|
14
|
+
* - `processed` — facts examined by the run (the denominator).
|
|
15
|
+
* - `changed` — facts that resulted in an insert/supersede (real mutations).
|
|
16
|
+
* - `skipped` — facts examined but left untouched (no-op / already current).
|
|
17
|
+
* - `conflicts` — facts that collided with higher-tier truth and were held
|
|
18
|
+
* back rather than overwriting it (truth hierarchy: user > sync > import).
|
|
19
|
+
* - `failures` — facts that errored and were neither applied nor cleanly
|
|
20
|
+
* skipped (the run's error budget).
|
|
21
|
+
*
|
|
22
|
+
* `sinceCursor` is the incremental watermark the run started from — the
|
|
23
|
+
* resume point handed to the adapter so it only fetches changes after that
|
|
24
|
+
* point. It is `null`/absent for a full (non-incremental) run. It is a plain
|
|
25
|
+
* resume watermark, not a paginated page cursor, so it is modeled as a bare
|
|
26
|
+
* nullable string rather than wrapped in a page envelope.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// SyncRunSummary — the outcome record of one adapter sync run
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export const SyncRunSummarySchema = z.object({
|
|
35
|
+
/** Unique identifier for this sync run within the org graph. */
|
|
36
|
+
id: z.string().uuid(),
|
|
37
|
+
/** ISO start of the run. Always set once a run has begun. */
|
|
38
|
+
startedAt: z.string().datetime(),
|
|
39
|
+
/** ISO end of the run; `null`/absent while the run is still in progress. */
|
|
40
|
+
endedAt: z.string().datetime().nullable().optional(),
|
|
41
|
+
/** Count of facts examined by the run (the denominator). */
|
|
42
|
+
processed: z.number().int().nonnegative(),
|
|
43
|
+
/** Count of facts that resulted in a real mutation (insert/supersede). */
|
|
44
|
+
changed: z.number().int().nonnegative(),
|
|
45
|
+
/** Count of facts examined but left untouched (no-op / already current). */
|
|
46
|
+
skipped: z.number().int().nonnegative(),
|
|
47
|
+
/** Count of facts held back due to a collision with higher-tier truth. */
|
|
48
|
+
conflicts: z.number().int().nonnegative(),
|
|
49
|
+
/** Count of facts that errored and were neither applied nor cleanly skipped. */
|
|
50
|
+
failures: z.number().int().nonnegative(),
|
|
51
|
+
/**
|
|
52
|
+
* Incremental watermark the run started from; `null`/absent for a full run.
|
|
53
|
+
* A resume point, not a paginated page cursor.
|
|
54
|
+
*/
|
|
55
|
+
sinceCursor: z.string().nullable().optional(),
|
|
56
|
+
});
|
|
57
|
+
export type SyncRunSummary = z.infer<typeof SyncRunSummarySchema>;
|