@company-semantics/contracts 20.0.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": "20.0.0",
3
+ "version": "20.1.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
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>;