@company-semantics/contracts 13.19.1 → 14.0.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": "13.19.1",
3
+ "version": "14.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -119,15 +119,15 @@
119
119
  "zod": "^4.4.3"
120
120
  },
121
121
  "devDependencies": {
122
- "@types/node": "^22.19.20",
122
+ "@types/node": "^25.9.3",
123
123
  "husky": "^9.1.7",
124
124
  "lint-staged": "^17.0.7",
125
125
  "markdownlint-cli2": "^0.22.1",
126
126
  "openapi-typescript": "^7.13.0",
127
- "prettier": "^3.8.3",
127
+ "prettier": "^3.8.4",
128
128
  "tsx": "^4.22.4",
129
129
  "typescript": "^5.8.3",
130
- "vitest": "^4.1.8",
130
+ "vitest": "^4.1.9",
131
131
  "yaml": "^2.9.0"
132
132
  },
133
133
  "pnpm": {
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '4c830693f0ba' as const;
3
- export const SPEC_HASH_FULL = '4c830693f0ba3d236b60cb5494fbfe4d62bdb8bf4cc6fedc1df0e2d1a5d7a2d6' as const;
2
+ export const SPEC_HASH = '16c3b86cc35a' as const;
3
+ export const SPEC_HASH_FULL = '16c3b86cc35a3f329766b702414438de4950430991a603372f1335206c96b600' as const;
@@ -1972,24 +1972,6 @@ export interface paths {
1972
1972
  patch?: never;
1973
1973
  trace?: never;
1974
1974
  };
1975
- "/api/org-units/{unitId}/head": {
1976
- parameters: {
1977
- query?: never;
1978
- header?: never;
1979
- path?: never;
1980
- cookie?: never;
1981
- };
1982
- get?: never;
1983
- /** Pin an explicit head (anchor person) for an org unit */
1984
- put: operations["setOrgUnitHead"];
1985
- post?: never;
1986
- /** Revert an org unit head to the system-inferred default */
1987
- delete: operations["revertOrgUnitHead"];
1988
- options?: never;
1989
- head?: never;
1990
- patch?: never;
1991
- trace?: never;
1992
- };
1993
1975
  "/api/org-units/{unitId}/my-authority": {
1994
1976
  parameters: {
1995
1977
  query?: never;
@@ -2111,6 +2093,23 @@ export interface paths {
2111
2093
  patch?: never;
2112
2094
  trace?: never;
2113
2095
  };
2096
+ "/api/org-units/{unitId}/open-roles/{roleId}/reports-to": {
2097
+ parameters: {
2098
+ query?: never;
2099
+ header?: never;
2100
+ path?: never;
2101
+ cookie?: never;
2102
+ };
2103
+ get?: never;
2104
+ /** Set or clear an open role's own reporting edge */
2105
+ put: operations["setOrgUnitOpenRoleReportsTo"];
2106
+ post?: never;
2107
+ delete?: never;
2108
+ options?: never;
2109
+ head?: never;
2110
+ patch?: never;
2111
+ trace?: never;
2112
+ };
2114
2113
  "/api/users/org-chart": {
2115
2114
  parameters: {
2116
2115
  query?: never;
@@ -4051,6 +4050,11 @@ export interface components {
4051
4050
  state: "allowed" | "blocked";
4052
4051
  requiredScope?: string;
4053
4052
  };
4053
+ manageSharing: {
4054
+ /** @enum {string} */
4055
+ state: "allowed" | "blocked";
4056
+ requiredScope?: string;
4057
+ };
4054
4058
  };
4055
4059
  workItem?: {
4056
4060
  transferOwnership: {
@@ -4058,6 +4062,18 @@ export interface components {
4058
4062
  state: "allowed" | "blocked";
4059
4063
  requiredScope?: string;
4060
4064
  };
4065
+ manageSharing: {
4066
+ /** @enum {string} */
4067
+ state: "allowed" | "blocked";
4068
+ requiredScope?: string;
4069
+ };
4070
+ };
4071
+ meetingRecording?: {
4072
+ manageSharing: {
4073
+ /** @enum {string} */
4074
+ state: "allowed" | "blocked";
4075
+ requiredScope?: string;
4076
+ };
4061
4077
  };
4062
4078
  };
4063
4079
  };
@@ -4305,8 +4321,6 @@ export interface components {
4305
4321
  count: number;
4306
4322
  userIds: string[];
4307
4323
  } | null;
4308
- headUserId: string | null;
4309
- headOrigin: ("user" | "inferred") | null;
4310
4324
  }[];
4311
4325
  };
4312
4326
  OrgUnitDescendantsResponse: {
@@ -4455,8 +4469,6 @@ export interface components {
4455
4469
  count: number;
4456
4470
  userIds: string[];
4457
4471
  } | null;
4458
- headUserId: string | null;
4459
- headOrigin: ("user" | "inferred") | null;
4460
4472
  }[];
4461
4473
  levelConfig: {
4462
4474
  /** Format: uuid */
@@ -4601,12 +4613,6 @@ export interface components {
4601
4613
  revokedAt: string | null;
4602
4614
  reason: string | null;
4603
4615
  };
4604
- UnitHeadResponse: {
4605
- /** Format: uuid */
4606
- unitId: string;
4607
- headUserId: string | null;
4608
- origin: ("user" | "inferred") | null;
4609
- };
4610
4616
  OrgUnitMyAuthorityResponse: {
4611
4617
  /** Format: uuid */
4612
4618
  unitId: string;
@@ -4676,6 +4682,7 @@ export interface components {
4676
4682
  /** @enum {string} */
4677
4683
  status: "open" | "hiring" | "filled" | "closed";
4678
4684
  filledByUserId: string | null;
4685
+ reportsToUserId: string | null;
4679
4686
  createdAt: string;
4680
4687
  updatedAt: string;
4681
4688
  }[];
@@ -4693,6 +4700,7 @@ export interface components {
4693
4700
  /** @enum {string} */
4694
4701
  status: "open" | "hiring" | "filled" | "closed";
4695
4702
  filledByUserId: string | null;
4703
+ reportsToUserId: string | null;
4696
4704
  createdAt: string;
4697
4705
  updatedAt: string;
4698
4706
  };
@@ -4720,14 +4728,7 @@ export interface components {
4720
4728
  /** Format: uuid */
4721
4729
  unitId: string;
4722
4730
  title: string | null;
4723
- }[];
4724
- unitHeads: {
4725
- /** Format: uuid */
4726
- unitId: string;
4727
- /** Format: uuid */
4728
- headUserId: string;
4729
- /** @enum {string} */
4730
- origin: "user" | "inferred";
4731
+ reportsToUserId: string | null;
4731
4732
  }[];
4732
4733
  };
4733
4734
  /** @description Polling snapshot of a generic ingestion operation. */
@@ -8480,57 +8481,6 @@ export interface operations {
8480
8481
  };
8481
8482
  };
8482
8483
  };
8483
- setOrgUnitHead: {
8484
- parameters: {
8485
- query?: never;
8486
- header?: never;
8487
- path: {
8488
- unitId: string;
8489
- };
8490
- cookie?: never;
8491
- };
8492
- requestBody: {
8493
- content: {
8494
- "application/json": {
8495
- /** Format: uuid */
8496
- userId: string;
8497
- };
8498
- };
8499
- };
8500
- responses: {
8501
- /** @description The pinned unit head */
8502
- 200: {
8503
- headers: {
8504
- [name: string]: unknown;
8505
- };
8506
- content: {
8507
- "application/json": components["schemas"]["UnitHeadResponse"];
8508
- };
8509
- };
8510
- };
8511
- };
8512
- revertOrgUnitHead: {
8513
- parameters: {
8514
- query?: never;
8515
- header?: never;
8516
- path: {
8517
- unitId: string;
8518
- };
8519
- cookie?: never;
8520
- };
8521
- requestBody?: never;
8522
- responses: {
8523
- /** @description The re-inferred unit head (or null when none resolves) */
8524
- 200: {
8525
- headers: {
8526
- [name: string]: unknown;
8527
- };
8528
- content: {
8529
- "application/json": components["schemas"]["UnitHeadResponse"];
8530
- };
8531
- };
8532
- };
8533
- };
8534
8484
  getMyOrgUnitAuthority: {
8535
8485
  parameters: {
8536
8486
  query?: never;
@@ -8777,6 +8727,35 @@ export interface operations {
8777
8727
  };
8778
8728
  };
8779
8729
  };
8730
+ setOrgUnitOpenRoleReportsTo: {
8731
+ parameters: {
8732
+ query?: never;
8733
+ header?: never;
8734
+ path: {
8735
+ unitId: string;
8736
+ roleId: string;
8737
+ };
8738
+ cookie?: never;
8739
+ };
8740
+ requestBody: {
8741
+ content: {
8742
+ "application/json": {
8743
+ userId: string | null;
8744
+ };
8745
+ };
8746
+ };
8747
+ responses: {
8748
+ /** @description Open role with updated reporting edge */
8749
+ 200: {
8750
+ headers: {
8751
+ [name: string]: unknown;
8752
+ };
8753
+ content: {
8754
+ "application/json": components["schemas"]["OpenRoleResponse"];
8755
+ };
8756
+ };
8757
+ };
8758
+ };
8780
8759
  getPeopleOrgChart: {
8781
8760
  parameters: {
8782
8761
  query?: never;
@@ -89,6 +89,7 @@ export const openApiRoutes = {
89
89
  '/api/org-units/{unitId}/open-roles': ['GET', 'POST'],
90
90
  '/api/org-units/{unitId}/open-roles/{roleId}': ['PATCH'],
91
91
  '/api/org-units/{unitId}/open-roles/{roleId}/fill': ['POST'],
92
+ '/api/org-units/{unitId}/open-roles/{roleId}/reports-to': ['PUT'],
92
93
  '/api/org-units/{unitId}/owners': ['GET'],
93
94
  '/api/org-units/{unitId}/permissions': ['GET'],
94
95
  '/api/org-units/{unitId}/relationships': ['GET', 'POST'],
@@ -67,8 +67,6 @@ export {
67
67
  PeopleOrgChartNodeSchema,
68
68
  PeopleOrgChartEdgeSchema,
69
69
  PeopleOrgChartOpenRoleSchema,
70
- UnitHeadOriginSchema,
71
- PeopleOrgChartUnitHeadSchema,
72
70
  PeopleOrgChartResponseSchema,
73
71
  } from "./people-org-chart";
74
72
  export type {
@@ -76,7 +74,5 @@ export type {
76
74
  PeopleOrgChartNode,
77
75
  PeopleOrgChartEdge,
78
76
  PeopleOrgChartOpenRole,
79
- UnitHeadOrigin,
80
- PeopleOrgChartUnitHead,
81
77
  PeopleOrgChartResponse,
82
78
  } from "./people-org-chart";
@@ -52,52 +52,37 @@ export type PeopleOrgChartEdge = z.infer<typeof PeopleOrgChartEdgeSchema>;
52
52
  // Open role — a persisted placeholder seat rendered as a dashed "Open role"
53
53
  // card in its unit's sibling column. Only active roles (open + hiring) are
54
54
  // included; filled/closed roles never appear here. `unitId` seats the card in
55
- // the right column and resolves its attach-person, mirroring how empty-unit
56
- // placeholders and pending-invite ghosts are positioned.
55
+ // the right column. `reportsToUserId` is the open role's OWN reporting edge
56
+ // (ADR-BE-291): it defaults to the role's creator and is editable/clearable, so
57
+ // a person-less seat is placed by reporting like everyone else (the chart owns
58
+ // placement, ADR-CTRL-159). Null → the role hangs in its unit's projection-only
59
+ // Unassigned bucket. This replaces the retired unit-head anchor (ADR-BE-284).
57
60
  // ---------------------------------------------------------------------------
58
61
 
59
62
  export const PeopleOrgChartOpenRoleSchema = z.object({
60
63
  id: z.string().uuid(),
61
64
  unitId: z.string().uuid(),
62
65
  title: z.string().nullable(),
66
+ reportsToUserId: z.string().uuid().nullable(),
63
67
  });
64
68
 
65
69
  export type PeopleOrgChartOpenRole = z.infer<
66
70
  typeof PeopleOrgChartOpenRoleSchema
67
71
  >;
68
72
 
69
- // ---------------------------------------------------------------------------
70
- // Unit head — the person a unit (and its person-less children: empty-unit
71
- // placeholders, pending-invite ghosts, open-role seats) anchors under in the
72
- // people chart. A presentation/anchor "tether" (ADR-BE-284), NOT authority.
73
- // `origin` distinguishes an explicit user choice from a system-inferred default
74
- // so the UI can render "Pinned" vs "Auto" and offer revert-to-auto. The client
75
- // prefers the head of a unit's PARENT when anchoring its person-less children,
76
- // falling back to the render-time heuristic only when no head row exists.
77
- // ---------------------------------------------------------------------------
78
-
79
- export const UnitHeadOriginSchema = z.enum(["user", "inferred"]);
80
- export type UnitHeadOrigin = z.infer<typeof UnitHeadOriginSchema>;
81
-
82
- export const PeopleOrgChartUnitHeadSchema = z.object({
83
- unitId: z.string().uuid(),
84
- headUserId: z.string().uuid(),
85
- origin: UnitHeadOriginSchema,
86
- });
87
-
88
- export type PeopleOrgChartUnitHead = z.infer<
89
- typeof PeopleOrgChartUnitHeadSchema
90
- >;
91
-
92
73
  // ---------------------------------------------------------------------------
93
74
  // GET /api/users/org-chart
94
75
  // ---------------------------------------------------------------------------
76
+ //
77
+ // Person-less cards (empty-unit placeholders, ghosts, open roles) no longer
78
+ // anchor under a stored unit-head "tether" (ADR-BE-284, retired by ADR-BE-292).
79
+ // Open roles carry their own `reportsToUserId` edge; placeholders fall back to
80
+ // the render-time reporting heuristic.
95
81
 
96
82
  export const PeopleOrgChartResponseSchema = z.object({
97
83
  nodes: z.array(PeopleOrgChartNodeSchema),
98
84
  edges: z.array(PeopleOrgChartEdgeSchema),
99
85
  openRoles: z.array(PeopleOrgChartOpenRoleSchema),
100
- unitHeads: z.array(PeopleOrgChartUnitHeadSchema),
101
86
  });
102
87
 
103
88
  export type PeopleOrgChartResponse = z.infer<
package/src/index.ts CHANGED
@@ -173,8 +173,6 @@ export {
173
173
  PeopleOrgChartNodeSchema,
174
174
  PeopleOrgChartEdgeSchema,
175
175
  PeopleOrgChartOpenRoleSchema,
176
- UnitHeadOriginSchema,
177
- PeopleOrgChartUnitHeadSchema,
178
176
  PeopleOrgChartResponseSchema,
179
177
  } from "./identity/index";
180
178
  export type {
@@ -182,8 +180,6 @@ export type {
182
180
  PeopleOrgChartNode,
183
181
  PeopleOrgChartEdge,
184
182
  PeopleOrgChartOpenRole,
185
- UnitHeadOrigin,
186
- PeopleOrgChartUnitHead,
187
183
  PeopleOrgChartResponse,
188
184
  } from "./identity/index";
189
185
 
@@ -358,6 +354,19 @@ export {
358
354
  // View authorization types (Phase 5 - ADR-APP-013)
359
355
  export type { AuthorizableView } from "./org/index";
360
356
 
357
+ // Org structure facts: provenance + home/membership separation (ADR-CONTRACTS-066)
358
+ export {
359
+ FactSourceTierSchema,
360
+ FACT_SOURCE_TIER_PRECEDENCE,
361
+ FactProvenanceSchema,
362
+ HomeAssignmentSchema,
363
+ } from "./org/index";
364
+ export type {
365
+ FactSourceTier,
366
+ FactProvenance,
367
+ HomeAssignment,
368
+ } from "./org/index";
369
+
361
370
  // Authority & Delegation vocabulary
362
371
  export {
363
372
  AuthoritySourceSchema,
@@ -98,8 +98,6 @@ describe("OrgUnitTreeNodeSchema", () => {
98
98
  memberCount: 5,
99
99
  openRoleCount: 1,
100
100
  missingAtNextLevel: null,
101
- headUserId: null,
102
- headOrigin: null,
103
101
  };
104
102
  expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
105
103
  expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 0 })).toThrow();
@@ -114,8 +112,6 @@ describe("OrgUnitTreeNodeSchema", () => {
114
112
  memberCount: 5,
115
113
  openRoleCount: 0,
116
114
  missingAtNextLevel: null,
117
- headUserId: null,
118
- headOrigin: null,
119
115
  };
120
116
  expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
121
117
  expect(() =>
@@ -133,6 +129,7 @@ describe("OpenRoleSchema", () => {
133
129
  targetStartDate: "2026-09-01",
134
130
  status: "open",
135
131
  filledByUserId: null,
132
+ reportsToUserId: null,
136
133
  createdAt: "2026-04-17T00:00:00Z",
137
134
  updatedAt: "2026-04-17T00:00:00Z",
138
135
  ...overrides,
package/src/org/index.ts CHANGED
@@ -87,6 +87,19 @@ export type {
87
87
  export type { AuthorizableView } from "./view-scopes";
88
88
  export { VIEW_SCOPE_MAP, getViewScope } from "./view-scopes";
89
89
 
90
+ // Org structure facts: provenance + home/membership separation (ADR-CONTRACTS-066)
91
+ export {
92
+ FactSourceTierSchema,
93
+ FACT_SOURCE_TIER_PRECEDENCE,
94
+ FactProvenanceSchema,
95
+ HomeAssignmentSchema,
96
+ } from "./structure-facts";
97
+ export type {
98
+ FactSourceTier,
99
+ FactProvenance,
100
+ HomeAssignment,
101
+ } from "./structure-facts";
102
+
90
103
  // Canonical OrgUnit tree ordering (PRD-00506)
91
104
  export type { TreeOrderableNode } from "./tree-ordering";
92
105
  export { orderTreeNodes } from "./tree-ordering";
@@ -949,16 +949,6 @@ export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
949
949
  */
950
950
  openRoleCount: z.number().int().min(0),
951
951
  missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
952
- /**
953
- * The unit head (ADR-BE-284): the person this unit (and its person-less
954
- * children) anchors under in the people chart — a presentation "tether", NOT
955
- * authority. `headUserId` is null when no head is resolved (e.g. an empty
956
- * unit with no members). `headOrigin` is `'user'` for an explicit choice,
957
- * `'inferred'` for a system default (so the management UI can show Pinned vs
958
- * Auto and offer revert-to-auto), or null when there is no head.
959
- */
960
- headUserId: z.string().uuid().nullable(),
961
- headOrigin: z.enum(["user", "inferred"]).nullable(),
962
952
  });
963
953
 
964
954
  export const OrgUnitMembershipSchema = z.object({
@@ -989,6 +979,10 @@ export const OpenRoleSchema = z.object({
989
979
  targetStartDate: z.string().nullable(),
990
980
  status: OpenRoleStatusSchema,
991
981
  filledByUserId: z.string().uuid().nullable(),
982
+ // The open role's own reporting edge (ADR-BE-291): defaults to its creator,
983
+ // editable/clearable. Drives chart placement now that the unit-head anchor is
984
+ // retired (ADR-BE-292). Null → unmanaged (Unassigned bucket).
985
+ reportsToUserId: z.string().uuid().nullable(),
992
986
  createdAt: z.string(),
993
987
  updatedAt: z.string(),
994
988
  });
@@ -1438,16 +1432,25 @@ export const CapabilitiesResponseSchema = z.object({
1438
1432
  orgUnit: z.object({
1439
1433
  manage: ActionCapabilitySchema,
1440
1434
  }),
1441
- // Entity ownership-transfer overrides (backend ADR-BE-259). Optional so
1442
- // older backends that don't derive them still validate.
1435
+ // Entity ownership-transfer overrides (backend ADR-BE-259) +
1436
+ // sharing-management custody (backend ADR-BE-288). Optional so older
1437
+ // backends that don't derive them still validate. manageSharing gates the
1438
+ // admin Share affordance on content the admin may not be able to view.
1443
1439
  companyMd: z
1444
1440
  .object({
1445
1441
  transferOwnership: ActionCapabilitySchema,
1442
+ manageSharing: ActionCapabilitySchema,
1446
1443
  })
1447
1444
  .optional(),
1448
1445
  workItem: z
1449
1446
  .object({
1450
1447
  transferOwnership: ActionCapabilitySchema,
1448
+ manageSharing: ActionCapabilitySchema,
1449
+ })
1450
+ .optional(),
1451
+ meetingRecording: z
1452
+ .object({
1453
+ manageSharing: ActionCapabilitySchema,
1451
1454
  })
1452
1455
  .optional(),
1453
1456
  })
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Org structure facts: provenance envelope + the home/membership separation.
3
+ *
4
+ * The org model is several INDEPENDENT graphs — the org-unit tree, the reporting
5
+ * graph, the home assignment, and the membership graph (and, later, positions).
6
+ * This module is the shared vocabulary for two cross-cutting ideas in that model
7
+ * (see ADR-CONTRACTS-066 and control ADR-CTRL-159):
8
+ *
9
+ * 1. Every structural fact carries provenance ({@link FactProvenanceSchema}).
10
+ * 2. "Home" is a dedicated ≤1-per-person relation ({@link HomeAssignmentSchema}),
11
+ * kept SEPARATE from `org_unit_memberships` — not a `kind` flag on memberships.
12
+ *
13
+ * Principles encoded here (do not drift from these):
14
+ * - Membership / home is an INPUT to authority derivation, NOT authority itself.
15
+ * `home member of unit` does not mean `owns unit`.
16
+ * - Facts are immutable; corrections SUPERSEDE prior facts (`supersedesFactId`)
17
+ * rather than mutating in place. This leaves room for event-sourcing later
18
+ * without forcing it now.
19
+ * - A truth hierarchy decides which fact wins: user > sync > import > inferred.
20
+ * - `source` is an OPEN string so new origins (scim, workday, csv, org_chart,
21
+ * google, slack, …) need no schema change; `tier` is the closed precedence axis.
22
+ */
23
+ import { z } from "zod";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Provenance
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * The precedence axis for a fact. Closed set, ordered highest-precedence first
31
+ * (see {@link FACT_SOURCE_TIER_PRECEDENCE}). The specific origin lives in the
32
+ * open `source` string; this enum is what comparison / "who wins" reasons over.
33
+ */
34
+ export const FactSourceTierSchema = z.enum([
35
+ "user",
36
+ "sync",
37
+ "import",
38
+ "inferred",
39
+ ]);
40
+ export type FactSourceTier = z.infer<typeof FactSourceTierSchema>;
41
+
42
+ /**
43
+ * Truth hierarchy, highest precedence first. A writer at a lower tier must never
44
+ * supersede a fact recorded at a higher tier (e.g. an org-chart import must not
45
+ * overwrite a `user`-entered reporting edge).
46
+ */
47
+ export const FACT_SOURCE_TIER_PRECEDENCE = [
48
+ "user",
49
+ "sync",
50
+ "import",
51
+ "inferred",
52
+ ] as const satisfies ReadonlyArray<FactSourceTier>;
53
+
54
+ /**
55
+ * Provenance envelope attached to a structural fact (reporting edge, org unit,
56
+ * membership, home assignment). The live value of any fact is the
57
+ * highest-precedence, non-superseded record.
58
+ */
59
+ export const FactProvenanceSchema = z.object({
60
+ /** Precedence axis used by the truth hierarchy. */
61
+ tier: FactSourceTierSchema,
62
+ /**
63
+ * Specific origin — OPEN set, e.g. `manual` | `org_chart` | `scim` | `hris` |
64
+ * `csv` | `google` | `slack`. New origins slot in without a schema change.
65
+ */
66
+ source: z.string().min(1),
67
+ /** Extractor/matcher certainty in [0,1]; `null` for human-entered facts. */
68
+ confidence: z.number().min(0).max(1).nullable(),
69
+ /**
70
+ * True once a human has corrected/confirmed the fact. A locked fact is sticky:
71
+ * lower-tier writers (imports, inference) must not supersede it.
72
+ */
73
+ locked: z.boolean(),
74
+ /**
75
+ * The fact this record supersedes (immutable correction history); `null`/absent
76
+ * for an original. Enables append+supersede without destructive updates.
77
+ */
78
+ supersedesFactId: z.string().uuid().nullable().optional(),
79
+ });
80
+ export type FactProvenance = z.infer<typeof FactProvenanceSchema>;
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // HomeAssignment (separate from membership)
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * A person's single structural home unit — at most one per person per org.
88
+ *
89
+ * SEPARATE relation from `org_unit_memberships` (NOT a `kind`/role flag on a
90
+ * shared table): the cardinalities differ on purpose — exactly one home, many
91
+ * memberships — which is the tell that they are different concepts. A shared
92
+ * `Membership(kind='home'|'member')` would inevitably grow
93
+ * `acting-home`/`temporary-home`/`future-home` flags.
94
+ *
95
+ * Home is an INPUT to authority derivation, sets `department`/`org` scope, and
96
+ * filters chart inclusion. It is NOT authority by itself (see Principles above).
97
+ * Supersedes the legacy `users.primary_unit_id` pointer (ADR-BE-120).
98
+ */
99
+ export const HomeAssignmentSchema = z.object({
100
+ personId: z.string().uuid(),
101
+ orgUnitId: z.string().uuid(),
102
+ /** Provenance of this home assignment. Optional on lightweight projections. */
103
+ provenance: FactProvenanceSchema.optional(),
104
+ });
105
+ export type HomeAssignment = z.infer<typeof HomeAssignmentSchema>;