@company-semantics/contracts 20.0.0 → 20.2.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 +2 -0
- package/src/content/schemas.ts +6 -0
- package/src/generated/openapi-routes.ts +14 -14
- 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/company-md.ts +14 -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 = '325ec2565e49' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '325ec2565e49e75abbf3b93a918e328dbe47a74586212e390cd0dff381a9913f' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -4147,6 +4147,7 @@ export interface components {
|
|
|
4147
4147
|
name: string;
|
|
4148
4148
|
} | null;
|
|
4149
4149
|
canEdit: boolean;
|
|
4150
|
+
inheritsFromId: string | null;
|
|
4150
4151
|
inheritsFrom: string | null;
|
|
4151
4152
|
sources: {
|
|
4152
4153
|
label: string;
|
|
@@ -4155,6 +4156,7 @@ export interface components {
|
|
|
4155
4156
|
referencedAt: string;
|
|
4156
4157
|
}[];
|
|
4157
4158
|
dependencies: {
|
|
4159
|
+
targetId: string;
|
|
4158
4160
|
targetSlug: string;
|
|
4159
4161
|
description: string;
|
|
4160
4162
|
}[];
|
package/src/content/schemas.ts
CHANGED
|
@@ -28,6 +28,9 @@ const CompanyMdSourceSchema = z.object({
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const CompanyMdDependencySchema = z.object({
|
|
31
|
+
// Stable id of the target doc — the resolution key (ADR-BE-315). `targetSlug`
|
|
32
|
+
// is presentation only; slugs are only parent-scoped unique post-ADR-BE-315.
|
|
33
|
+
targetId: z.string(),
|
|
31
34
|
targetSlug: z.string(),
|
|
32
35
|
description: z.string(),
|
|
33
36
|
});
|
|
@@ -85,6 +88,9 @@ export const CompanyMdDocResponseSchema = z.object({
|
|
|
85
88
|
owningUnitId: z.string().nullable(),
|
|
86
89
|
owner: CompanyMdPersonSchema.nullable(),
|
|
87
90
|
canEdit: z.boolean(),
|
|
91
|
+
// Stable id of the inherited-from doc — the resolution key (mirrors the
|
|
92
|
+
// resolved parentId). `inheritsFrom` is presentation only (ADR-BE-315).
|
|
93
|
+
inheritsFromId: z.string().nullable(),
|
|
88
94
|
inheritsFrom: z.string().nullable(),
|
|
89
95
|
sources: z.array(CompanyMdSourceSchema),
|
|
90
96
|
dependencies: z.array(CompanyMdDependencySchema),
|
|
@@ -22,20 +22,20 @@ export const openApiRoutes = {
|
|
|
22
22
|
'/api/chats/{id}/messages/{messageId}': ['DELETE'],
|
|
23
23
|
'/api/chats/{id}/pin': ['DELETE', 'POST'],
|
|
24
24
|
'/api/company-md/context-bank': ['POST'],
|
|
25
|
-
'/api/company-md/docs/{
|
|
26
|
-
'/api/company-md/docs/{
|
|
27
|
-
'/api/company-md/docs/{
|
|
28
|
-
'/api/company-md/docs/{
|
|
29
|
-
'/api/company-md/docs/{
|
|
30
|
-
'/api/company-md/docs/{
|
|
31
|
-
'/api/company-md/docs/{
|
|
32
|
-
'/api/company-md/docs/{
|
|
33
|
-
'/api/company-md/docs/{
|
|
34
|
-
'/api/company-md/docs/{
|
|
35
|
-
'/api/company-md/docs/{
|
|
36
|
-
'/api/company-md/docs/{
|
|
37
|
-
'/api/company-md/docs/{
|
|
38
|
-
'/api/company-md/docs/{
|
|
25
|
+
'/api/company-md/docs/{id}': ['GET'],
|
|
26
|
+
'/api/company-md/docs/{id}/content': ['PUT'],
|
|
27
|
+
'/api/company-md/docs/{id}/context-bank': ['GET'],
|
|
28
|
+
'/api/company-md/docs/{id}/context-bank/associate': ['POST'],
|
|
29
|
+
'/api/company-md/docs/{id}/context-bank/retry': ['POST'],
|
|
30
|
+
'/api/company-md/docs/{id}/context-bank/upload': ['POST'],
|
|
31
|
+
'/api/company-md/docs/{id}/context-bank/{contextDocId}': ['DELETE'],
|
|
32
|
+
'/api/company-md/docs/{id}/context-bank/{contextDocId}/order': ['PATCH'],
|
|
33
|
+
'/api/company-md/docs/{id}/sharing': ['GET'],
|
|
34
|
+
'/api/company-md/docs/{id}/sharing/acl': ['POST'],
|
|
35
|
+
'/api/company-md/docs/{id}/sharing/acl/{aclId}': ['DELETE', 'PUT'],
|
|
36
|
+
'/api/company-md/docs/{id}/sharing/policy': ['PUT'],
|
|
37
|
+
'/api/company-md/docs/{id}/title': ['PUT'],
|
|
38
|
+
'/api/company-md/docs/{id}/transfer-owner': ['POST'],
|
|
39
39
|
'/api/company-md/extract': ['POST'],
|
|
40
40
|
'/api/company-md/ownership/reassign': ['POST'],
|
|
41
41
|
'/api/company-md/ownership/reassign/suggestion': ['GET'],
|
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>;
|
package/src/org/company-md.ts
CHANGED
|
@@ -80,6 +80,13 @@ export interface CompanyMdSource {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export interface CompanyMdDependency {
|
|
83
|
+
/**
|
|
84
|
+
* Stable id of the target doc — the identity used to resolve the reference.
|
|
85
|
+
* Post-ADR-BE-315 slugs are only parent-scoped unique, so `targetSlug` cannot
|
|
86
|
+
* identify a doc unambiguously; resolve by `targetId`, display `targetSlug`.
|
|
87
|
+
*/
|
|
88
|
+
readonly targetId: string;
|
|
89
|
+
/** Human-readable slug of the target doc — presentation/label only. */
|
|
83
90
|
readonly targetSlug: string;
|
|
84
91
|
readonly description: string;
|
|
85
92
|
}
|
|
@@ -137,6 +144,13 @@ export interface CompanyMdDocCollaborators {
|
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
export interface CompanyMdDocRelations {
|
|
147
|
+
/**
|
|
148
|
+
* Stable id of the doc this one inherits from — the identity used to resolve
|
|
149
|
+
* the reference (mirrors the resolved `parentId`). Resolve by `inheritsFromId`,
|
|
150
|
+
* display `inheritsFrom`. See ADR-BE-315: slugs are only parent-scoped unique.
|
|
151
|
+
*/
|
|
152
|
+
readonly inheritsFromId: string | null;
|
|
153
|
+
/** Human-readable slug of the inherited-from doc — presentation/label only. */
|
|
140
154
|
readonly inheritsFrom: string | null;
|
|
141
155
|
readonly sources: readonly CompanyMdSource[];
|
|
142
156
|
readonly dependencies: readonly CompanyMdDependency[];
|
|
@@ -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>;
|