@company-semantics/contracts 19.0.0 → 19.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "19.0.0",
3
+ "version": "19.1.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -377,6 +377,32 @@ export type {
377
377
  HomeAssignment,
378
378
  } from "./org/index";
379
379
 
380
+ // Occupancy: the holds_position relation (M:N, co-occupancy first-class).
381
+ // Vacancy = a position with no active occupancy. (ADR-CONT-083 / ADR-CTRL-182)
382
+ export { OccupancyStatusSchema, OccupancySchema } from "./org/index";
383
+ export type { OccupancyStatus, Occupancy } from "./org/index";
384
+
385
+ // Position: the org seat (planned/open/filled/closed) — a seat, not a person
386
+ // or a title. Carries a FactProvenance envelope. (ADR-CONT-083 / ADR-CTRL-182)
387
+ export { PositionStatusSchema, PositionSchema } from "./org/index";
388
+ export type { PositionStatus, Position } from "./org/index";
389
+
390
+ // Position reporting: the canonical seat -> manager-seat reporting edge. The
391
+ // person hierarchy derives from this composed with occupancy. (ADR-CONT-083 / ADR-CTRL-182)
392
+ export {
393
+ PositionReportingRelationshipTypeSchema,
394
+ PositionReportingSchema,
395
+ } from "./org/index";
396
+ export type {
397
+ PositionReportingRelationshipType,
398
+ PositionReporting,
399
+ } from "./org/index";
400
+
401
+ // Source authority: per-field ownership — who may WRITE a given entity field.
402
+ // Orthogonal to provenance (who reported). (ADR-CONT-083 / ADR-CTRL-182)
403
+ export { SourceAuthoritySchema } from "./org/index";
404
+ export type { SourceAuthority } from "./org/index";
405
+
380
406
  // Authority & Delegation vocabulary
381
407
  export {
382
408
  AuthoritySourceSchema,
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>;