@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "19.2.0",
3
+ "version": "20.1.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '814c99a18543' as const;
3
- export const SPEC_HASH_FULL = '814c99a18543f448daaed5312db276f7704a9b63710c512b1a298be5fc652105' as const;
2
+ export const SPEC_HASH = '79972d6725ff' as const;
3
+ export const SPEC_HASH_FULL = '79972d6725ff3dfd29ee68945c8006039eddc196eadaeb0bcd77abeeebf2b066' as const;
@@ -1338,14 +1338,14 @@ export interface paths {
1338
1338
  patch?: never;
1339
1339
  trace?: never;
1340
1340
  };
1341
- "/api/company-md/docs/{slug}": {
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 slug */
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/{slug}/content": {
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/{slug}/title": {
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/{slug}/context-bank": {
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/{slug}/context-bank/associate": {
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/{slug}/context-bank/{contextDocSlug}": {
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/{slug}/context-bank/{id}/order": {
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/{parentSlug}/context-bank/upload": {
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/{slug}/context-bank/retry": {
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/{slug}/transfer-owner": {
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/{slug}/sharing": {
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/{slug}/sharing/policy": {
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/{slug}/sharing/acl": {
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/{slug}/sharing/acl/{aclId}": {
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
- parentDocSlug: string;
4190
+ parentDocId: string;
4191
4191
  };
4192
4192
  AssociateContextDocRequest: {
4193
- contextDocSlug: string;
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
- slug: string;
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
- slug: string;
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
- slug: string;
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
- slug: string;
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
- slug: string;
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
- slug: string;
7587
- contextDocSlug: string;
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
- parentSlug: string;
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
- slug: string;
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 slug */
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
- slug: string;
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
- slug: string;
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
- slug: string;
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
- slug: string;
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
- slug: string;
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
- slug: string;
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>;