@company-semantics/contracts 19.0.0 → 19.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/index.ts +37 -1
- package/src/integrations/README.md +46 -0
- package/src/integrations/index.ts +8 -0
- package/src/integrations/schemas.ts +72 -0
- package/src/org/README.md +14 -0
- package/src/org/__tests__/occupancy.test.ts +74 -0
- package/src/org/__tests__/position-reporting.test.ts +88 -0
- package/src/org/__tests__/positions.test.ts +70 -0
- package/src/org/__tests__/source-authority.test.ts +64 -0
- package/src/org/index.ts +26 -0
- package/src/org/occupancy.ts +69 -0
- package/src/org/position-reporting.ts +63 -0
- package/src/org/positions.ts +65 -0
- package/src/org/source-authority.ts +44 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -23,7 +23,12 @@ export type InsightConfidence = "low" | "medium" | "high";
|
|
|
23
23
|
export type InvariantPhase = "observe" | "stabilize" | "enforce";
|
|
24
24
|
|
|
25
25
|
// Integration partner categories — surface grouping in settings UIs
|
|
26
|
-
export const INTEGRATION_CATEGORIES = [
|
|
26
|
+
export const INTEGRATION_CATEGORIES = [
|
|
27
|
+
"Meetings",
|
|
28
|
+
"Comms",
|
|
29
|
+
"Docs",
|
|
30
|
+
"HR",
|
|
31
|
+
] as const;
|
|
27
32
|
export type IntegrationCategory = (typeof INTEGRATION_CATEGORIES)[number];
|
|
28
33
|
|
|
29
34
|
// System diagram types
|
|
@@ -377,6 +382,32 @@ export type {
|
|
|
377
382
|
HomeAssignment,
|
|
378
383
|
} from "./org/index";
|
|
379
384
|
|
|
385
|
+
// Occupancy: the holds_position relation (M:N, co-occupancy first-class).
|
|
386
|
+
// Vacancy = a position with no active occupancy. (ADR-CONT-083 / ADR-CTRL-182)
|
|
387
|
+
export { OccupancyStatusSchema, OccupancySchema } from "./org/index";
|
|
388
|
+
export type { OccupancyStatus, Occupancy } from "./org/index";
|
|
389
|
+
|
|
390
|
+
// Position: the org seat (planned/open/filled/closed) — a seat, not a person
|
|
391
|
+
// or a title. Carries a FactProvenance envelope. (ADR-CONT-083 / ADR-CTRL-182)
|
|
392
|
+
export { PositionStatusSchema, PositionSchema } from "./org/index";
|
|
393
|
+
export type { PositionStatus, Position } from "./org/index";
|
|
394
|
+
|
|
395
|
+
// Position reporting: the canonical seat -> manager-seat reporting edge. The
|
|
396
|
+
// person hierarchy derives from this composed with occupancy. (ADR-CONT-083 / ADR-CTRL-182)
|
|
397
|
+
export {
|
|
398
|
+
PositionReportingRelationshipTypeSchema,
|
|
399
|
+
PositionReportingSchema,
|
|
400
|
+
} from "./org/index";
|
|
401
|
+
export type {
|
|
402
|
+
PositionReportingRelationshipType,
|
|
403
|
+
PositionReporting,
|
|
404
|
+
} from "./org/index";
|
|
405
|
+
|
|
406
|
+
// Source authority: per-field ownership — who may WRITE a given entity field.
|
|
407
|
+
// Orthogonal to provenance (who reported). (ADR-CONT-083 / ADR-CTRL-182)
|
|
408
|
+
export { SourceAuthoritySchema } from "./org/index";
|
|
409
|
+
export type { SourceAuthority } from "./org/index";
|
|
410
|
+
|
|
380
411
|
// Authority & Delegation vocabulary
|
|
381
412
|
export {
|
|
382
413
|
AuthoritySourceSchema,
|
|
@@ -812,3 +843,8 @@ export * from "./permissions";
|
|
|
812
843
|
// Single source of truth for upload formats; the backend normalizer joins its
|
|
813
844
|
// private dispatch fields locally, the app derives its accept-set + copy.
|
|
814
845
|
export * from "./ingestion";
|
|
846
|
+
|
|
847
|
+
// Third-party integration vocabulary (PRD-00793)
|
|
848
|
+
// HrConnectionStatus / HrConnectInput — see ./integrations/schemas.ts.
|
|
849
|
+
// Pairs with the `HR` member of INTEGRATION_CATEGORIES above.
|
|
850
|
+
export * from "./integrations";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# integrations/
|
|
2
|
+
|
|
3
|
+
Shared vocabulary for third-party integration surfaces.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The cross-repo contract for HR-system integrations. The backend owns the
|
|
8
|
+
provider sync and persistence; the app renders the connection state and submits
|
|
9
|
+
the connect form. Both repos share the same `HrConnectionStatus` projection and
|
|
10
|
+
`HrConnectInput` shape so the boundary stays stable across independent releases.
|
|
11
|
+
|
|
12
|
+
The `HR` member of `INTEGRATION_CATEGORIES` (package root) names this category
|
|
13
|
+
alongside `Meetings`, `Comms`, and `Docs`.
|
|
14
|
+
|
|
15
|
+
## Invariants
|
|
16
|
+
|
|
17
|
+
- `HrConnectionStatusSchema` is a read projection: count and sync fields are
|
|
18
|
+
`null` until the first successful sync (a connected, never-synced integration).
|
|
19
|
+
- `HrConnectInputSchema` carries only the customer's provider `subdomain`; no
|
|
20
|
+
secrets or provider credentials live in contracts.
|
|
21
|
+
- Pure vocabulary — `zod` is the only runtime import (vocabulary-guard).
|
|
22
|
+
|
|
23
|
+
<!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
|
|
24
|
+
|
|
25
|
+
## Public API
|
|
26
|
+
|
|
27
|
+
- `HrConnectInput` _(type)_
|
|
28
|
+
- `HrConnectInputSchema` — Input submitted by the app to initiate an HR provider connection.
|
|
29
|
+
- `HrConnectionStatus` _(type)_
|
|
30
|
+
- `HrConnectionStatusSchema` — Health of the most recent HR provider sync.
|
|
31
|
+
|
|
32
|
+
<!-- END GENERATED: readme-public-api -->
|
|
33
|
+
|
|
34
|
+
<!-- BEGIN GENERATED: readme-dependencies — derived from code by `pnpm readme-api`. Do not edit. -->
|
|
35
|
+
|
|
36
|
+
## Dependencies
|
|
37
|
+
|
|
38
|
+
**Internal domains:**
|
|
39
|
+
|
|
40
|
+
_None._
|
|
41
|
+
|
|
42
|
+
**External packages:**
|
|
43
|
+
|
|
44
|
+
- `zod`
|
|
45
|
+
|
|
46
|
+
<!-- END GENERATED: readme-dependencies -->
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Third-party integration vocabulary barrel (PRD-00793).
|
|
3
|
+
*
|
|
4
|
+
* @see ./schemas.ts for the full schema definitions and invariants.
|
|
5
|
+
*/
|
|
6
|
+
export { HrConnectionStatusSchema, HrConnectInputSchema } from "./schemas";
|
|
7
|
+
|
|
8
|
+
export type { HrConnectionStatus, HrConnectInput } from "./schemas";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Third-party integration shared vocabulary (PRD-00793).
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas describing the cross-repo contract for HR-system integrations.
|
|
5
|
+
* The `HR` integration category joins the existing `INTEGRATION_CATEGORIES`
|
|
6
|
+
* tuple in the package root; these schemas describe the connection-status
|
|
7
|
+
* projection the backend exposes and the connect input the app submits.
|
|
8
|
+
*
|
|
9
|
+
* Lives in contracts because the integration surface crosses backend (which
|
|
10
|
+
* owns the provider sync + persistence) and app (which renders the connection
|
|
11
|
+
* state and the connect form), and breaking it would require coordinated
|
|
12
|
+
* releases (see the promotion rule in CLAUDE.md).
|
|
13
|
+
*
|
|
14
|
+
* Per the durable user preference `feedback_structured_metadata_not_json_strings`,
|
|
15
|
+
* descriptions live in `.meta({ description })` — structured metadata, not
|
|
16
|
+
* stringified JSON blobs on the schema.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// HR connection status
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Health of the most recent HR provider sync. `null` when no sync has run yet
|
|
26
|
+
* (a freshly connected, never-synced integration).
|
|
27
|
+
*/
|
|
28
|
+
export const HrConnectionStatusSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
connected: z.boolean().meta({
|
|
31
|
+
description: "Whether an HR provider is currently connected.",
|
|
32
|
+
}),
|
|
33
|
+
employeeCount: z.number().int().nonnegative().nullable().meta({
|
|
34
|
+
description:
|
|
35
|
+
"Employees mirrored from the HR provider; null when never synced.",
|
|
36
|
+
}),
|
|
37
|
+
departmentCount: z.number().int().nonnegative().nullable().meta({
|
|
38
|
+
description:
|
|
39
|
+
"Departments mirrored from the HR provider; null when never synced.",
|
|
40
|
+
}),
|
|
41
|
+
lastSyncAt: z.string().datetime().nullable().meta({
|
|
42
|
+
description: "ISO timestamp of the last successful sync; null if never.",
|
|
43
|
+
}),
|
|
44
|
+
syncHealth: z.enum(["healthy", "degraded", "error"]).nullable().meta({
|
|
45
|
+
description:
|
|
46
|
+
"Health of the most recent sync; null when no sync has run yet.",
|
|
47
|
+
}),
|
|
48
|
+
})
|
|
49
|
+
.meta({
|
|
50
|
+
description: "Connection-status projection for an HR integration.",
|
|
51
|
+
});
|
|
52
|
+
export type HrConnectionStatus = z.infer<typeof HrConnectionStatusSchema>;
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// HR connect input
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Input submitted by the app to initiate an HR provider connection. The
|
|
60
|
+
* `subdomain` identifies the customer's tenant on the provider (e.g. the
|
|
61
|
+
* BambooHR company subdomain).
|
|
62
|
+
*/
|
|
63
|
+
export const HrConnectInputSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
subdomain: z.string().meta({
|
|
66
|
+
description: "Customer tenant subdomain on the HR provider.",
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
.meta({
|
|
70
|
+
description: "Input to initiate an HR provider connection.",
|
|
71
|
+
});
|
|
72
|
+
export type HrConnectInput = z.infer<typeof HrConnectInputSchema>;
|
package/src/org/README.md
CHANGED
|
@@ -105,6 +105,10 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
105
105
|
- `MissingAtNextLevel` _(type)_
|
|
106
106
|
- `MissingAtNextLevelSchema` — Active users home directly at this unit who have no level-(depth+1) home assignment within its subtree.
|
|
107
107
|
- `ORG_UNITS_ROUTES` — Canonical route path constants for the `/api/org-units` surface.
|
|
108
|
+
- `Occupancy` _(type)_
|
|
109
|
+
- `OccupancySchema`
|
|
110
|
+
- `OccupancyStatus` _(type)_
|
|
111
|
+
- `OccupancyStatusSchema` — Lifecycle of a single occupancy of a position.
|
|
108
112
|
- `OidcValidationResult` _(type)_ — Result of validating an OIDC discovery URL.
|
|
109
113
|
- `OidcValidationResultDto` _(type)_ — Result of validating an OIDC discovery URL.
|
|
110
114
|
- `OidcValidationResultSchema`
|
|
@@ -204,6 +208,14 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
204
208
|
- `PermissionAuditEntry` _(type)_ — Single entry in the permission change audit log.
|
|
205
209
|
- `Phase3AuditAction` _(type)_ — Audit actions for Phase 3 workspace expansion features.
|
|
206
210
|
- `Phase4AuditAction` _(type)_ — Audit actions for Phase 4 enterprise identity features.
|
|
211
|
+
- `Position` _(type)_
|
|
212
|
+
- `PositionReporting` _(type)_
|
|
213
|
+
- `PositionReportingRelationshipType` _(type)_
|
|
214
|
+
- `PositionReportingRelationshipTypeSchema` — The kind of reporting line.
|
|
215
|
+
- `PositionReportingSchema`
|
|
216
|
+
- `PositionSchema`
|
|
217
|
+
- `PositionStatus` _(type)_
|
|
218
|
+
- `PositionStatusSchema` — Lifecycle of a seat's EXISTENCE — distinct from a hiring pipeline.
|
|
207
219
|
- `ProviderStatus` _(type)_ — Provider-level configuration lifecycle.
|
|
208
220
|
- `ProviderSuggestion` _(type)_ — MX-based provider suggestion for SSO setup.
|
|
209
221
|
- `RemoveMemberRequest` _(type)_ — Request payload for removing a member from the workspace.
|
|
@@ -222,6 +234,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
222
234
|
- `SetMemberManagerResponseSchema` — Response for setting (PATCH) or clearing (DELETE) a member's solid-line manager — the person-to-person…
|
|
223
235
|
- `SharePolicy` _(type)_ — Document sharing policy. - restricted: Only explicit ACL + owning unit + org admins - orgread: All org…
|
|
224
236
|
- `ShareState` _(type)_ — Complete sharing state for a document.
|
|
237
|
+
- `SourceAuthority` _(type)_
|
|
238
|
+
- `SourceAuthoritySchema`
|
|
225
239
|
- `SsoCredentialStatus` _(type)_ — Boolean credential presence indicators (never expose actual values).
|
|
226
240
|
- `SsoDiscoveryConfig` _(type)_ — OIDC discovery configuration for the SSO provider.
|
|
227
241
|
- `SsoEnforcementStatus` _(type)_ — SSO enforcement status for the workspace.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { OccupancySchema, OccupancyStatusSchema } from "../occupancy.js";
|
|
4
|
+
|
|
5
|
+
describe("OccupancySchema", () => {
|
|
6
|
+
const base = {
|
|
7
|
+
id: "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
|
8
|
+
orgId: "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b",
|
|
9
|
+
positionId: "9c5b94b1-35ad-49bb-b118-8e8fc24abf80",
|
|
10
|
+
personId: "7d793037-a076-4f50-9c0e-0e4c0b9d5b1f",
|
|
11
|
+
status: "active",
|
|
12
|
+
provenance: {
|
|
13
|
+
tier: "user",
|
|
14
|
+
source: "manual",
|
|
15
|
+
confidence: null,
|
|
16
|
+
locked: true,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
it("parses a minimal active occupancy without dates", () => {
|
|
21
|
+
const occ = OccupancySchema.parse(base);
|
|
22
|
+
expect(occ.status).toBe("active");
|
|
23
|
+
expect(occ.startsAt).toBeUndefined();
|
|
24
|
+
expect(occ.endsAt).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("accepts null and ISO datetime for startsAt/endsAt", () => {
|
|
28
|
+
expect(
|
|
29
|
+
OccupancySchema.parse({ ...base, startsAt: null, endsAt: null }).endsAt,
|
|
30
|
+
).toBeNull();
|
|
31
|
+
const dated = OccupancySchema.parse({
|
|
32
|
+
...base,
|
|
33
|
+
startsAt: "2026-01-01T00:00:00.000Z",
|
|
34
|
+
endsAt: "2026-06-01T00:00:00.000Z",
|
|
35
|
+
});
|
|
36
|
+
expect(dated.startsAt).toBe("2026-01-01T00:00:00.000Z");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects a non-ISO datetime", () => {
|
|
40
|
+
expect(() =>
|
|
41
|
+
OccupancySchema.parse({ ...base, startsAt: "2026-01-01" }),
|
|
42
|
+
).toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects non-uuid ids", () => {
|
|
46
|
+
expect(() => OccupancySchema.parse({ ...base, id: "nope" })).toThrow();
|
|
47
|
+
expect(() =>
|
|
48
|
+
OccupancySchema.parse({ ...base, positionId: "nope" }),
|
|
49
|
+
).toThrow();
|
|
50
|
+
expect(() =>
|
|
51
|
+
OccupancySchema.parse({ ...base, personId: "nope" }),
|
|
52
|
+
).toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("requires a provenance envelope (structure fact)", () => {
|
|
56
|
+
const { provenance: _omitted, ...withoutProvenance } = base;
|
|
57
|
+
expect(() => OccupancySchema.parse(withoutProvenance)).toThrow();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects an unknown status", () => {
|
|
61
|
+
expect(() =>
|
|
62
|
+
OccupancySchema.parse({ ...base, status: "vacant" }),
|
|
63
|
+
).toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("OccupancyStatusSchema is the closed pending/active/ended set", () => {
|
|
67
|
+
expect(OccupancyStatusSchema.options).toEqual([
|
|
68
|
+
"pending",
|
|
69
|
+
"active",
|
|
70
|
+
"ended",
|
|
71
|
+
]);
|
|
72
|
+
expect(() => OccupancyStatusSchema.parse("filled")).toThrow();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
PositionReportingSchema,
|
|
4
|
+
PositionReportingRelationshipTypeSchema,
|
|
5
|
+
} from "../position-reporting.js";
|
|
6
|
+
|
|
7
|
+
const validEdge = {
|
|
8
|
+
id: "11111111-1111-4111-8111-111111111111",
|
|
9
|
+
orgId: "22222222-2222-4222-8222-222222222222",
|
|
10
|
+
reportPositionId: "33333333-3333-4333-8333-333333333333",
|
|
11
|
+
managerPositionId: "44444444-4444-4444-8444-444444444444",
|
|
12
|
+
relationshipType: "solid" as const,
|
|
13
|
+
provenance: {
|
|
14
|
+
tier: "user" as const,
|
|
15
|
+
source: "manual",
|
|
16
|
+
confidence: null,
|
|
17
|
+
locked: true,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("PositionReportingSchema", () => {
|
|
22
|
+
it("accepts a valid position reporting edge", () => {
|
|
23
|
+
expect(PositionReportingSchema.parse(validEdge)).toEqual(validEdge);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("accepts an edge without provenance (lightweight projection)", () => {
|
|
27
|
+
const { provenance: _omit, ...lightweight } = validEdge;
|
|
28
|
+
expect(PositionReportingSchema.safeParse(lightweight).success).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("accepts both relationship types", () => {
|
|
32
|
+
for (const relationshipType of ["solid", "dotted"]) {
|
|
33
|
+
expect(
|
|
34
|
+
PositionReportingSchema.safeParse({ ...validEdge, relationshipType })
|
|
35
|
+
.success,
|
|
36
|
+
).toBe(true);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects an unknown relationship type", () => {
|
|
41
|
+
expect(
|
|
42
|
+
PositionReportingSchema.safeParse({
|
|
43
|
+
...validEdge,
|
|
44
|
+
relationshipType: "matrix",
|
|
45
|
+
}).success,
|
|
46
|
+
).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("rejects a non-uuid reportPositionId", () => {
|
|
50
|
+
expect(
|
|
51
|
+
PositionReportingSchema.safeParse({
|
|
52
|
+
...validEdge,
|
|
53
|
+
reportPositionId: "not-a-uuid",
|
|
54
|
+
}).success,
|
|
55
|
+
).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("rejects a non-uuid managerPositionId", () => {
|
|
59
|
+
expect(
|
|
60
|
+
PositionReportingSchema.safeParse({
|
|
61
|
+
...validEdge,
|
|
62
|
+
managerPositionId: "not-a-uuid",
|
|
63
|
+
}).success,
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rejects a missing orgId", () => {
|
|
68
|
+
const { orgId: _omit, ...noOrg } = validEdge;
|
|
69
|
+
expect(PositionReportingSchema.safeParse(noOrg).success).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("PositionReportingRelationshipTypeSchema", () => {
|
|
74
|
+
it("accepts solid and dotted", () => {
|
|
75
|
+
expect(PositionReportingRelationshipTypeSchema.parse("solid")).toBe(
|
|
76
|
+
"solid",
|
|
77
|
+
);
|
|
78
|
+
expect(PositionReportingRelationshipTypeSchema.parse("dotted")).toBe(
|
|
79
|
+
"dotted",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("rejects any other value", () => {
|
|
84
|
+
expect(
|
|
85
|
+
PositionReportingRelationshipTypeSchema.safeParse("solid-line").success,
|
|
86
|
+
).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { PositionSchema, PositionStatusSchema } from "../positions.js";
|
|
4
|
+
|
|
5
|
+
describe("PositionSchema", () => {
|
|
6
|
+
const base = {
|
|
7
|
+
id: "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
|
8
|
+
orgId: "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b",
|
|
9
|
+
unitId: "9c5b94b1-35ad-49bb-b118-8e8fc24abf80",
|
|
10
|
+
title: "Staff Engineer — Platform",
|
|
11
|
+
status: "open",
|
|
12
|
+
lifecycleOwner: "org_design",
|
|
13
|
+
provenance: {
|
|
14
|
+
tier: "user",
|
|
15
|
+
source: "manual",
|
|
16
|
+
confidence: null,
|
|
17
|
+
locked: true,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it("parses a valid open position", () => {
|
|
22
|
+
const position = PositionSchema.parse(base);
|
|
23
|
+
expect(position.status).toBe("open");
|
|
24
|
+
expect(position.title).toBe("Staff Engineer — Platform");
|
|
25
|
+
expect(position.lifecycleOwner).toBe("org_design");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("accepts every lifecycle status", () => {
|
|
29
|
+
for (const status of ["planned", "open", "filled", "closed"] as const) {
|
|
30
|
+
expect(PositionSchema.parse({ ...base, status }).status).toBe(status);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("rejects a non-uuid id, orgId, or unitId", () => {
|
|
35
|
+
expect(() => PositionSchema.parse({ ...base, id: "nope" })).toThrow();
|
|
36
|
+
expect(() => PositionSchema.parse({ ...base, orgId: "nope" })).toThrow();
|
|
37
|
+
expect(() => PositionSchema.parse({ ...base, unitId: "nope" })).toThrow();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects an empty title or lifecycleOwner", () => {
|
|
41
|
+
expect(() => PositionSchema.parse({ ...base, title: "" })).toThrow();
|
|
42
|
+
expect(() =>
|
|
43
|
+
PositionSchema.parse({ ...base, lifecycleOwner: "" }),
|
|
44
|
+
).toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects an invalid status and a hiring-only status", () => {
|
|
48
|
+
expect(() =>
|
|
49
|
+
PositionSchema.parse({ ...base, status: "archived" }),
|
|
50
|
+
).toThrow();
|
|
51
|
+
// 'hiring' belongs to OpenRoleStatus, not the seat-existence lifecycle.
|
|
52
|
+
expect(() => PositionSchema.parse({ ...base, status: "hiring" })).toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("requires a provenance envelope (Position is a structure fact)", () => {
|
|
56
|
+
const { provenance: _provenance, ...withoutProvenance } = base;
|
|
57
|
+
expect(() => PositionSchema.parse(withoutProvenance)).toThrow();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("PositionStatusSchema", () => {
|
|
62
|
+
it("is a closed set distinct from OpenRoleStatus", () => {
|
|
63
|
+
expect(PositionStatusSchema.options).toEqual([
|
|
64
|
+
"planned",
|
|
65
|
+
"open",
|
|
66
|
+
"filled",
|
|
67
|
+
"closed",
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { SourceAuthoritySchema } from "../source-authority.js";
|
|
4
|
+
|
|
5
|
+
describe("SourceAuthoritySchema", () => {
|
|
6
|
+
const base = {
|
|
7
|
+
entityType: "position",
|
|
8
|
+
entityId: "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
|
9
|
+
field: "title",
|
|
10
|
+
systemOfRecord: "hris",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it("parses a valid source-authority record", () => {
|
|
14
|
+
const authority = SourceAuthoritySchema.parse(base);
|
|
15
|
+
expect(authority.entityType).toBe("position");
|
|
16
|
+
expect(authority.field).toBe("title");
|
|
17
|
+
expect(authority.systemOfRecord).toBe("hris");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts any entity kind as an open string", () => {
|
|
21
|
+
for (const entityType of [
|
|
22
|
+
"position",
|
|
23
|
+
"occupancy",
|
|
24
|
+
"person",
|
|
25
|
+
"future_kind",
|
|
26
|
+
]) {
|
|
27
|
+
expect(
|
|
28
|
+
SourceAuthoritySchema.parse({ ...base, entityType }).entityType,
|
|
29
|
+
).toBe(entityType);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("accepts any system of record as an open string", () => {
|
|
34
|
+
for (const systemOfRecord of ["hris", "manual", "scim", "future_system"]) {
|
|
35
|
+
expect(
|
|
36
|
+
SourceAuthoritySchema.parse({ ...base, systemOfRecord }).systemOfRecord,
|
|
37
|
+
).toBe(systemOfRecord);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects a non-uuid entityId", () => {
|
|
42
|
+
expect(() =>
|
|
43
|
+
SourceAuthoritySchema.parse({ ...base, entityId: "nope" }),
|
|
44
|
+
).toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects an empty entityType, field, or systemOfRecord", () => {
|
|
48
|
+
expect(() =>
|
|
49
|
+
SourceAuthoritySchema.parse({ ...base, entityType: "" }),
|
|
50
|
+
).toThrow();
|
|
51
|
+
expect(() => SourceAuthoritySchema.parse({ ...base, field: "" })).toThrow();
|
|
52
|
+
expect(() =>
|
|
53
|
+
SourceAuthoritySchema.parse({ ...base, systemOfRecord: "" }),
|
|
54
|
+
).toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("does not carry a provenance envelope (distinct from provenance)", () => {
|
|
58
|
+
const parsed = SourceAuthoritySchema.parse({
|
|
59
|
+
...base,
|
|
60
|
+
provenance: { tier: "user", source: "manual" },
|
|
61
|
+
});
|
|
62
|
+
expect("provenance" in parsed).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
package/src/org/index.ts
CHANGED
|
@@ -100,6 +100,32 @@ export type {
|
|
|
100
100
|
HomeAssignment,
|
|
101
101
|
} from "./structure-facts";
|
|
102
102
|
|
|
103
|
+
// Occupancy: the holds_position relation (M:N, co-occupancy first-class).
|
|
104
|
+
// Vacancy = a position with no active occupancy. (ADR-CONT-083 / ADR-CTRL-182)
|
|
105
|
+
export { OccupancyStatusSchema, OccupancySchema } from "./occupancy";
|
|
106
|
+
export type { OccupancyStatus, Occupancy } from "./occupancy";
|
|
107
|
+
|
|
108
|
+
// Position: the org seat (planned/open/filled/closed) — a seat, not a person
|
|
109
|
+
// or a title. Carries a FactProvenance envelope. (ADR-CONT-083 / ADR-CTRL-182)
|
|
110
|
+
export { PositionStatusSchema, PositionSchema } from "./positions";
|
|
111
|
+
export type { PositionStatus, Position } from "./positions";
|
|
112
|
+
|
|
113
|
+
// Position reporting: the canonical seat -> manager-seat reporting edge. The
|
|
114
|
+
// person hierarchy derives from this composed with occupancy. (ADR-CONT-083 / ADR-CTRL-182)
|
|
115
|
+
export {
|
|
116
|
+
PositionReportingRelationshipTypeSchema,
|
|
117
|
+
PositionReportingSchema,
|
|
118
|
+
} from "./position-reporting";
|
|
119
|
+
export type {
|
|
120
|
+
PositionReportingRelationshipType,
|
|
121
|
+
PositionReporting,
|
|
122
|
+
} from "./position-reporting";
|
|
123
|
+
|
|
124
|
+
// Source authority: per-field ownership — who may WRITE a given entity field.
|
|
125
|
+
// Orthogonal to provenance (who reported). (ADR-CONT-083 / ADR-CTRL-182)
|
|
126
|
+
export { SourceAuthoritySchema } from "./source-authority";
|
|
127
|
+
export type { SourceAuthority } from "./source-authority";
|
|
128
|
+
|
|
103
129
|
// Canonical OrgUnit tree ordering (PRD-00506)
|
|
104
130
|
export type { TreeOrderableNode } from "./tree-ordering";
|
|
105
131
|
export { orderTreeNodes } from "./tree-ordering";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Occupancy: the `holds_position` relation between a Person and a Position.
|
|
3
|
+
*
|
|
4
|
+
* A Position is a SEAT in the org graph; an Occupancy records that a particular
|
|
5
|
+
* Person holds that seat over some interval. The relation is many-to-many and
|
|
6
|
+
* co-occupancy is FIRST-CLASS (ADR-CTRL-182 clause 2 / contracts ADR-CONT-083):
|
|
7
|
+
*
|
|
8
|
+
* - A single Position MAY be held by several People at once (job share,
|
|
9
|
+
* transition overlap, interim + permanent), so co-occupancy is a normal
|
|
10
|
+
* state, not an error to be reconciled away.
|
|
11
|
+
* - A single Person MAY hold several Positions at once (one human, many seats).
|
|
12
|
+
*
|
|
13
|
+
* VACANCY is the absence of any active Occupancy for a Position — a Position with
|
|
14
|
+
* no `status: "active"` Occupancy is vacant. Vacancy is therefore derived, never
|
|
15
|
+
* stored: do not add a `vacant` flag to Position; query Occupancy instead.
|
|
16
|
+
*
|
|
17
|
+
* Occupancy is a STRUCTURE FACT: it carries a {@link FactProvenanceSchema}
|
|
18
|
+
* envelope so the truth hierarchy (user > sync > import > inferred) and the
|
|
19
|
+
* append+supersede correction model apply to it exactly as they do to reporting
|
|
20
|
+
* edges, memberships, and home assignments. This is the distinction from
|
|
21
|
+
* Person/IdentityLink, which are identity vocabulary and carry no provenance.
|
|
22
|
+
*
|
|
23
|
+
* `positionId` is a forward reference to a Position seat id (a bare uuid here);
|
|
24
|
+
* `personId` references the org-graph {@link PersonSchema} node, not a User
|
|
25
|
+
* account.
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
|
|
29
|
+
import { FactProvenanceSchema } from "./structure-facts";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Occupancy status — closed lifecycle axis
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lifecycle of a single occupancy of a position. Closed set.
|
|
37
|
+
*
|
|
38
|
+
* - `pending` — a future hold not yet in effect (e.g. an accepted offer with a
|
|
39
|
+
* start date in the future). Does NOT make the position occupied.
|
|
40
|
+
* - `active` — the person currently holds the position. The only status that
|
|
41
|
+
* counts toward occupancy; a position with no `active` occupancy is vacant.
|
|
42
|
+
* - `ended` — the hold has concluded (kept as history, never deleted).
|
|
43
|
+
*/
|
|
44
|
+
export const OccupancyStatusSchema = z.enum(["pending", "active", "ended"]);
|
|
45
|
+
export type OccupancyStatus = z.infer<typeof OccupancyStatusSchema>;
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Occupancy — the holds_position structure fact
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export const OccupancySchema = z.object({
|
|
52
|
+
/** Unique identifier for this occupancy record within the org graph. */
|
|
53
|
+
id: z.string().uuid(),
|
|
54
|
+
/** Owning organization (tenant) this occupancy belongs to. */
|
|
55
|
+
orgId: z.string().uuid(),
|
|
56
|
+
/** The position (seat) being held. Forward reference to a Position id. */
|
|
57
|
+
positionId: z.string().uuid(),
|
|
58
|
+
/** The person holding the position — an org-graph node, not a User account. */
|
|
59
|
+
personId: z.string().uuid(),
|
|
60
|
+
/** Lifecycle of this hold; only `active` makes the position occupied. */
|
|
61
|
+
status: OccupancyStatusSchema,
|
|
62
|
+
/** ISO start of the hold; `null`/absent when unknown or open-started. */
|
|
63
|
+
startsAt: z.string().datetime().nullable().optional(),
|
|
64
|
+
/** ISO end of the hold; `null`/absent while the hold is still open. */
|
|
65
|
+
endsAt: z.string().datetime().nullable().optional(),
|
|
66
|
+
/** Provenance of this occupancy fact (truth hierarchy + supersede model). */
|
|
67
|
+
provenance: FactProvenanceSchema,
|
|
68
|
+
});
|
|
69
|
+
export type Occupancy = z.infer<typeof OccupancySchema>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position Reporting Vocabulary
|
|
3
|
+
*
|
|
4
|
+
* A PositionReporting record is the CANONICAL reporting edge of the org graph:
|
|
5
|
+
* a position reports to a manager position. The person-level "who reports to
|
|
6
|
+
* whom" hierarchy is DERIVED from this edge composed with occupancy (who holds
|
|
7
|
+
* each position) — see ADR-CTRL-182 clause 3. Modelling reporting at the
|
|
8
|
+
* position layer (seat → seat) rather than the person layer keeps the chart
|
|
9
|
+
* stable across re-orgs and vacancies: the edge survives even when a seat is
|
|
10
|
+
* temporarily unfilled.
|
|
11
|
+
*
|
|
12
|
+
* `relationshipType` distinguishes a `solid` line (primary, single chain of
|
|
13
|
+
* accountability) from a `dotted` line (secondary / matrixed reporting). It is
|
|
14
|
+
* defined locally here as a closed two-member enum so the org module stays
|
|
15
|
+
* self-contained and takes on no dependency on the identity domain.
|
|
16
|
+
*
|
|
17
|
+
* NOTE ON PLACEMENT / PROVENANCE: ADR-CONT-083 sketches the Position family in
|
|
18
|
+
* the identity domain and describes authority via a separate SourceAuthority
|
|
19
|
+
* qualifier. This module instead lives in the org domain and carries the shared
|
|
20
|
+
* {@link FactProvenanceSchema} envelope — consistent with the other structural
|
|
21
|
+
* facts in this domain (reporting edges, home assignments). The reporting edge
|
|
22
|
+
* is a structure fact, so it records its own provenance.
|
|
23
|
+
*
|
|
24
|
+
* @see ADR-CONT-083 for the Position/Occupancy/PositionReporting vocabulary
|
|
25
|
+
* @see structure-facts.ts for the FactProvenance envelope
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { FactProvenanceSchema } from "./structure-facts";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Reporting relationship type — solid (primary) vs dotted (matrixed)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The kind of reporting line. Closed set: `solid` is the primary accountability
|
|
36
|
+
* chain (a position has exactly one solid manager); `dotted` is a secondary /
|
|
37
|
+
* matrixed line. Defined locally to keep the org module self-contained.
|
|
38
|
+
*/
|
|
39
|
+
export const PositionReportingRelationshipTypeSchema = z.enum([
|
|
40
|
+
"solid",
|
|
41
|
+
"dotted",
|
|
42
|
+
]);
|
|
43
|
+
export type PositionReportingRelationshipType = z.infer<
|
|
44
|
+
typeof PositionReportingRelationshipTypeSchema
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// PositionReporting — position → manager position canonical edge
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export const PositionReportingSchema = z.object({
|
|
52
|
+
id: z.string().uuid(),
|
|
53
|
+
orgId: z.string().uuid(),
|
|
54
|
+
// The reporting (subordinate) position — the seat that reports up.
|
|
55
|
+
reportPositionId: z.string().uuid(),
|
|
56
|
+
// The manager position this seat reports to.
|
|
57
|
+
managerPositionId: z.string().uuid(),
|
|
58
|
+
// Solid (primary) vs dotted (matrixed) reporting line.
|
|
59
|
+
relationshipType: PositionReportingRelationshipTypeSchema,
|
|
60
|
+
// Provenance of this reporting edge. Optional on lightweight projections.
|
|
61
|
+
provenance: FactProvenanceSchema.optional(),
|
|
62
|
+
});
|
|
63
|
+
export type PositionReporting = z.infer<typeof PositionReportingSchema>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position Vocabulary — the org seat.
|
|
3
|
+
*
|
|
4
|
+
* A Position is a SEAT in the org, NOT a person and NOT a title (ADR-CTRL-182
|
|
5
|
+
* clause 1). It is the slot a Person can occupy: it exists independently of
|
|
6
|
+
* whoever fills it, can be `planned` before anyone is hired, sits `open` while
|
|
7
|
+
* vacant, becomes `filled` when a Person occupies it, and is `closed` when the
|
|
8
|
+
* seat is retired. `title` is a human-facing LABEL for the seat (e.g. "Staff
|
|
9
|
+
* Engineer — Platform"); it is descriptive metadata, never the occupant's
|
|
10
|
+
* identity.
|
|
11
|
+
*
|
|
12
|
+
* Position is a structure fact (it lives in a unit and is sourced/imported like
|
|
13
|
+
* other org-structure edges), so unlike `Person` it carries a
|
|
14
|
+
* {@link FactProvenanceSchema} envelope. See control ADR-CTRL-182 for the
|
|
15
|
+
* org-model rationale and the contracts-side ADR-CONT-083 for the API addition.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
import { FactProvenanceSchema } from "./structure-facts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Position lifecycle status
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lifecycle of a seat's EXISTENCE — distinct from a hiring pipeline. A seat is
|
|
27
|
+
* `planned` (intended, not yet active), `open` (active and vacant), `filled` (a
|
|
28
|
+
* Person occupies it), or `closed` (retired). This is deliberately NOT
|
|
29
|
+
* `OpenRoleStatusSchema` (which models the hiring funnel and has `hiring`): a
|
|
30
|
+
* Position's status is about whether the seat is, not about recruiting for it.
|
|
31
|
+
*/
|
|
32
|
+
export const PositionStatusSchema = z.enum([
|
|
33
|
+
"planned",
|
|
34
|
+
"open",
|
|
35
|
+
"filled",
|
|
36
|
+
"closed",
|
|
37
|
+
]);
|
|
38
|
+
export type PositionStatus = z.infer<typeof PositionStatusSchema>;
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Position — the org seat
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export const PositionSchema = z.object({
|
|
45
|
+
/** Unique identifier for the seat within the org graph. */
|
|
46
|
+
id: z.string().uuid(),
|
|
47
|
+
/** Owning organization (tenant) this seat belongs to. */
|
|
48
|
+
orgId: z.string().uuid(),
|
|
49
|
+
/** The org unit the seat lives in (references `org_units`). */
|
|
50
|
+
unitId: z.string().uuid(),
|
|
51
|
+
/** Human-facing LABEL for the seat, NOT the occupant's identity. */
|
|
52
|
+
title: z.string().min(1),
|
|
53
|
+
/** Lifecycle of the seat's existence. */
|
|
54
|
+
status: PositionStatusSchema,
|
|
55
|
+
/**
|
|
56
|
+
* Who owns the seat's EXISTENCE (its creation, retitling, retirement) — an
|
|
57
|
+
* OPEN string (no enum) so new owner kinds need no schema change, mirroring
|
|
58
|
+
* the `provider`/`source` open-string idiom. This is about the seat, not who
|
|
59
|
+
* fills it.
|
|
60
|
+
*/
|
|
61
|
+
lifecycleOwner: z.string().min(1),
|
|
62
|
+
/** Provenance of this seat as a structure fact. */
|
|
63
|
+
provenance: FactProvenanceSchema,
|
|
64
|
+
});
|
|
65
|
+
export type Position = z.infer<typeof PositionSchema>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SourceAuthority Vocabulary — the per-field ownership axis.
|
|
3
|
+
*
|
|
4
|
+
* A SourceAuthority binds a single FIELD of a single org-graph entity to its
|
|
5
|
+
* `systemOfRecord` — the authority that may WRITE that field (ADR-CTRL-182
|
|
6
|
+
* clause 4). It answers "who owns this value?", a question that is deliberately
|
|
7
|
+
* DISTINCT from provenance: provenance ({@link FactProvenanceSchema}) records
|
|
8
|
+
* who *reported* a fact and how confident it is, whereas SourceAuthority
|
|
9
|
+
* records who is *allowed to change* it. The two axes are orthogonal — a fact
|
|
10
|
+
* may be reported manually yet owned by an HRIS — so this schema does NOT embed
|
|
11
|
+
* or reference the provenance envelope.
|
|
12
|
+
*
|
|
13
|
+
* Like `Person`, SourceAuthority is identity/ownership vocabulary, not a
|
|
14
|
+
* structure fact, so it carries no provenance envelope. `entityType` and
|
|
15
|
+
* `systemOfRecord` are OPEN strings (no enum) so new entity kinds and new
|
|
16
|
+
* systems of record need no schema change, mirroring the `provider`/`source`
|
|
17
|
+
* open-string idiom used elsewhere in the org model. See control ADR-CTRL-182.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// SourceAuthority — per-field ownership: who may WRITE a given entity field
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export const SourceAuthoritySchema = z.object({
|
|
26
|
+
/**
|
|
27
|
+
* Kind of org-graph entity the owned field belongs to (e.g. "position",
|
|
28
|
+
* "occupancy", "person"). An OPEN string so new entity kinds need no schema
|
|
29
|
+
* change.
|
|
30
|
+
*/
|
|
31
|
+
entityType: z.string().min(1),
|
|
32
|
+
/** Identifier of the specific entity instance that owns the field. */
|
|
33
|
+
entityId: z.string().uuid(),
|
|
34
|
+
/** Name of the single field this authority record governs. */
|
|
35
|
+
field: z.string().min(1),
|
|
36
|
+
/**
|
|
37
|
+
* The authority that may WRITE this field — the system of record. An OPEN
|
|
38
|
+
* string (no enum) so new systems of record need no schema change. This is
|
|
39
|
+
* about who may change the value, NOT who reported it (provenance).
|
|
40
|
+
*/
|
|
41
|
+
systemOfRecord: z.string().min(1),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type SourceAuthority = z.infer<typeof SourceAuthoritySchema>;
|