@company-semantics/contracts 86.1.0 → 88.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 +14 -10
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +50 -262
- package/src/chat/README.md +8 -20
- package/src/chat/__tests__/compaction-boundary.test.ts +1 -0
- package/src/chat/__tests__/continuity.test.ts +1 -0
- package/src/chat/__tests__/discussion-scope.test.ts +0 -7
- package/src/chat/__tests__/message-mentions.test.ts +1 -12
- package/src/chat/__tests__/multi-party.test.ts +15 -6
- package/src/chat/__tests__/page-context.test.ts +3 -3
- package/src/chat/enums.ts +24 -11
- package/src/chat/index.ts +4 -16
- package/src/chat/schemas.ts +9 -71
- package/src/chat/types.ts +25 -77
- package/src/generated/openapi-routes.ts +1 -4
- package/src/guards/README.md +0 -1
- package/src/guards/config.ts +1 -10
- package/src/guards/evolution-config.ts +1 -18
- package/src/guards/index.ts +0 -1
- package/src/index.ts +15 -1279
- package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +4 -4
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +50 -49
- package/src/notifications/__tests__/fixtures.ts +7 -7
- package/src/notifications/__tests__/render-snapshot.test.ts +7 -7
- package/src/notifications/kinds/chat-shared.ts +11 -5
- package/src/notifications/payloads.ts +5 -5
- package/src/org/README.md +0 -212
- package/src/org/__tests__/view-scopes.test.ts +0 -2
- package/src/org/authority-schemas.ts +242 -0
- package/src/org/index.ts +26 -249
- package/src/org/invite-schemas.ts +596 -0
- package/src/org/org-governance-schemas.ts +198 -0
- package/src/org/org-unit-api-schemas.ts +226 -0
- package/src/org/org-unit-schemas.ts +223 -0
- package/src/org/ownership-transfer-types.ts +51 -0
- package/src/org/schemas.ts +18 -1658
- package/src/org/sso-stepper-types.ts +81 -0
- package/src/org/types.ts +15 -115
- package/src/org/view-scopes.ts +0 -1
- package/src/org/workspace-admin-schemas.ts +240 -0
- package/src/root-exports/README.md +91 -0
- package/src/root-exports/agent-runtime.ts +213 -0
- package/src/root-exports/collaboration.ts +247 -0
- package/src/root-exports/delivery.ts +218 -0
- package/src/root-exports/org-structure.ts +214 -0
- package/src/root-exports/org-workspace.ts +254 -0
- package/src/root-exports/platform.ts +205 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org Governance Response Schemas
|
|
3
|
+
*
|
|
4
|
+
* Org domains, deletion eligibility, ownership transfer, system events and the
|
|
5
|
+
* org AI-usage report.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `./schemas.ts`, which re-exports this module unchanged — import
|
|
8
|
+
* from `./schemas` (or the `./org` entry), not from this file directly.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { IsoDateTime } from "../api/primitives";
|
|
12
|
+
import { MoneyMicrosSchema } from "../billing/vocabulary";
|
|
13
|
+
import { AI_FEATURES } from "../usage/features";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// OrgDomain sub-schema
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const OrgDomainSchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
orgId: z.string(),
|
|
22
|
+
domain: z.string(),
|
|
23
|
+
status: z.enum(["pending", "verified"]),
|
|
24
|
+
verificationMethod: z.enum(["dns_txt", "email", "idp"]),
|
|
25
|
+
verificationToken: z.string().optional(),
|
|
26
|
+
verifiedAt: z.string().nullable(),
|
|
27
|
+
createdAt: z.string(),
|
|
28
|
+
verifiedBy: z
|
|
29
|
+
.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
30
|
+
.optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// GET /api/workspace/domains/:id/verify (single domain)
|
|
35
|
+
// POST /api/workspace/domains/:id/verify (returns verified OrgDomain)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export const DomainResponseSchema = OrgDomainSchema;
|
|
39
|
+
|
|
40
|
+
export type DomainResponse = z.infer<typeof DomainResponseSchema>;
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// GET /api/workspace/domains
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export const DomainListResponseSchema = z.array(OrgDomainSchema);
|
|
47
|
+
|
|
48
|
+
export type DomainListResponse = z.infer<typeof DomainListResponseSchema>;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// GET /api/org/deletion-eligibility
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const MemberClassificationSchema = z.object({
|
|
55
|
+
userId: z.string(),
|
|
56
|
+
email: z.string(),
|
|
57
|
+
suggestedFate: z.enum(["REMOVE_FROM_ORG", "DELETE_ACCOUNT", "BLOCKED"]),
|
|
58
|
+
reason: z.string(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const OrgDeletionStatusSchema = z.object({
|
|
62
|
+
orgId: z.string(),
|
|
63
|
+
eligible: z.boolean(),
|
|
64
|
+
isPersonalOrg: z.boolean(),
|
|
65
|
+
ownerStatus: z.string().optional(),
|
|
66
|
+
deletionRequestedAt: z.string().optional(),
|
|
67
|
+
classifications: z.array(MemberClassificationSchema),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export type OrgDeletionStatus = z.infer<typeof OrgDeletionStatusSchema>;
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// POST /api/org/transfer-ownership (initiate)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export const OwnershipTransferResponseSchema = z.object({
|
|
77
|
+
transferId: z.string(),
|
|
78
|
+
expiresAt: z.string(),
|
|
79
|
+
message: z.string(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export type OwnershipTransferResponse = z.infer<
|
|
83
|
+
typeof OwnershipTransferResponseSchema
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// POST /api/org/transfer-ownership/preview
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
export const OwnershipTransferPreviewSchema = z.object({
|
|
91
|
+
workspaceName: z.string(),
|
|
92
|
+
initiatorName: z.string(),
|
|
93
|
+
initiatorEmail: z.string(),
|
|
94
|
+
note: z.string().optional(),
|
|
95
|
+
expiresAt: z.string(),
|
|
96
|
+
token: z.string(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export type OwnershipTransferPreview = z.infer<
|
|
100
|
+
typeof OwnershipTransferPreviewSchema
|
|
101
|
+
>;
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// GET /api/org/system-events
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
const OrgSystemEventSchema = z.object({
|
|
108
|
+
id: z.string(),
|
|
109
|
+
type: z.string(),
|
|
110
|
+
payload: z.unknown(),
|
|
111
|
+
createdAt: z.string(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const OrgSystemEventsListSchema = z.object({
|
|
115
|
+
events: z.array(OrgSystemEventSchema),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export type OrgSystemEventsList = z.infer<typeof OrgSystemEventsListSchema>;
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// POST /api/org/system-events/:id/acknowledge
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export const AcknowledgeSystemEventResponseSchema = z.object({
|
|
125
|
+
success: z.literal(true),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export type AcknowledgeSystemEventResponse = z.infer<
|
|
129
|
+
typeof AcknowledgeSystemEventResponseSchema
|
|
130
|
+
>;
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// GET /api/orgs/:orgId/ai-usage
|
|
134
|
+
//
|
|
135
|
+
// INV-USAGE-LEDGER: every request, token and dollar here is read from the
|
|
136
|
+
// settled billing ledger (`billing_usage_events`) — one row per billed provider
|
|
137
|
+
// call, whatever feature made it. A `request` is one settled billable provider
|
|
138
|
+
// invocation (retries and fallbacks are separate requests), every amount is
|
|
139
|
+
// the customer charge in integer micro-USD, and `tokens` is input + output +
|
|
140
|
+
// cache read + cache write (reasoning is a subset of output, never added).
|
|
141
|
+
// `chatRuns` alone comes from runtime telemetry: duration and failure
|
|
142
|
+
// diagnostics for chat turns, carrying no money.
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
const OrgAiUsageTotalsSchema = z.object({
|
|
146
|
+
requests: z.number().int().min(0),
|
|
147
|
+
tokens: z.number().int().min(0),
|
|
148
|
+
customerChargeMicros: MoneyMicrosSchema,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const OrgChatRunProfileSchema = z.object({
|
|
152
|
+
profile: z.string(),
|
|
153
|
+
executions: z.number().int().min(0),
|
|
154
|
+
percentOfTotal: z.number(),
|
|
155
|
+
avgDurationMs: z.number(),
|
|
156
|
+
failureRate: z.number(),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export const OrgUsageResponseSchema = z.object({
|
|
160
|
+
/** The org's billing window: `start`/`end` in UTC, days bucketed in `timezone`. */
|
|
161
|
+
period: z.object({
|
|
162
|
+
start: IsoDateTime,
|
|
163
|
+
end: IsoDateTime,
|
|
164
|
+
timezone: z.string(),
|
|
165
|
+
}),
|
|
166
|
+
summary: OrgAiUsageTotalsSchema,
|
|
167
|
+
daily: z.array(
|
|
168
|
+
z.object({
|
|
169
|
+
date: z.string(),
|
|
170
|
+
requests: z.number().int().min(0),
|
|
171
|
+
customerChargeMicros: MoneyMicrosSchema,
|
|
172
|
+
}),
|
|
173
|
+
),
|
|
174
|
+
byFeature: z.array(
|
|
175
|
+
OrgAiUsageTotalsSchema.extend({ feature: z.enum(AI_FEATURES) }),
|
|
176
|
+
),
|
|
177
|
+
byModel: z.array(
|
|
178
|
+
OrgAiUsageTotalsSchema.extend({
|
|
179
|
+
provider: z.string(),
|
|
180
|
+
modelId: z.string(),
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
/** Attributed usage only: system work (no user) counts in the totals, not here. */
|
|
184
|
+
byUser: z.array(
|
|
185
|
+
OrgAiUsageTotalsSchema.extend({
|
|
186
|
+
userId: z.string(),
|
|
187
|
+
email: z.string().nullable(),
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
chatRuns: z.object({
|
|
191
|
+
executions: z.number().int().min(0),
|
|
192
|
+
avgDurationMs: z.number(),
|
|
193
|
+
failureRate: z.number(),
|
|
194
|
+
byProfile: z.array(OrgChatRunProfileSchema),
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export type OrgUsageResponse = z.infer<typeof OrgUsageResponseSchema>;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org Unit API Schemas
|
|
3
|
+
*
|
|
4
|
+
* Unit relationships and level config, the org-unit response envelopes, open-role
|
|
5
|
+
* and unit-permission request/response schemas, and their inferred types.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `./schemas.ts`, which re-exports this module unchanged — import
|
|
8
|
+
* from `./schemas` (or the `./org` entry), not from this file directly.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { ORG_UNIT_ABSOLUTE_MAX_DEPTH } from "./org-depth";
|
|
12
|
+
import {
|
|
13
|
+
MissingAtNextLevelSchema,
|
|
14
|
+
OpenRoleSchema,
|
|
15
|
+
OrgUnitMembershipSchema,
|
|
16
|
+
OrgUnitRelationshipRoleSchema,
|
|
17
|
+
OrgUnitRelationshipTypeSchema,
|
|
18
|
+
OrgUnitSchema,
|
|
19
|
+
OrgUnitTreeNodeSchema,
|
|
20
|
+
UnitMembershipRoleSchema,
|
|
21
|
+
} from "./org-unit-schemas";
|
|
22
|
+
|
|
23
|
+
export const OrgUnitRelationshipSchema = z.object({
|
|
24
|
+
id: z.string().uuid(),
|
|
25
|
+
orgId: z.string().uuid(),
|
|
26
|
+
fromUnit: z.string().uuid(),
|
|
27
|
+
toUnit: z.string().uuid(),
|
|
28
|
+
type: OrgUnitRelationshipTypeSchema,
|
|
29
|
+
role: OrgUnitRelationshipRoleSchema,
|
|
30
|
+
contextKey: z.string().nullable(),
|
|
31
|
+
metadata: z.record(z.string(), z.unknown()),
|
|
32
|
+
createdAt: z.string(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const OrgLevelIconSchema = z.enum([
|
|
36
|
+
"city",
|
|
37
|
+
"buildings",
|
|
38
|
+
"users-four",
|
|
39
|
+
"users-three",
|
|
40
|
+
"users",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export const OrgLevelConfigSchema = z.object({
|
|
44
|
+
orgId: z.string().uuid(),
|
|
45
|
+
depth: z.number().int().min(1).max(ORG_UNIT_ABSOLUTE_MAX_DEPTH),
|
|
46
|
+
label: z.string().min(1).nullable(),
|
|
47
|
+
labelPlural: z.string().min(1).nullable(),
|
|
48
|
+
icon: OrgLevelIconSchema.nullable(),
|
|
49
|
+
createdAt: z.string(),
|
|
50
|
+
updatedAt: z.string(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// --- Response schemas (request schemas live in backend per ADR-CONT-044) ---
|
|
54
|
+
|
|
55
|
+
export const OrgUnitResponseSchema = z.object({ unit: OrgUnitSchema });
|
|
56
|
+
|
|
57
|
+
export const OrgUnitTreeResponseSchema = z.object({
|
|
58
|
+
nodes: z.array(OrgUnitTreeNodeSchema),
|
|
59
|
+
levelConfig: z.array(OrgLevelConfigSchema),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const OrgUnitChildrenResponseSchema = z.object({
|
|
63
|
+
parentId: z.string().uuid(),
|
|
64
|
+
children: z.array(OrgUnitTreeNodeSchema),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const OrgUnitAncestorsResponseSchema = z.object({
|
|
68
|
+
unitId: z.string().uuid(),
|
|
69
|
+
ancestors: z.array(OrgUnitSchema),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const OrgUnitDescendantsResponseSchema = z.object({
|
|
73
|
+
rootId: z.string().uuid(),
|
|
74
|
+
descendants: z.array(OrgUnitSchema),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const OrgUnitRelationshipsResponseSchema = z.object({
|
|
78
|
+
unitId: z.string().uuid(),
|
|
79
|
+
incoming: z.array(OrgUnitRelationshipSchema),
|
|
80
|
+
outgoing: z.array(OrgUnitRelationshipSchema),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const OrgLevelConfigResponseSchema = z.object({
|
|
84
|
+
entries: z.array(OrgLevelConfigSchema),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const OrgUnitMembershipResponseSchema = z.object({
|
|
88
|
+
membership: OrgUnitMembershipSchema,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const OrgUnitMembershipListResponseSchema = z.object({
|
|
92
|
+
unitId: z.string().uuid(),
|
|
93
|
+
memberships: z.array(OrgUnitMembershipSchema),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Open roles (PRD open-roles)
|
|
98
|
+
// Request schemas are colocated here (a narrow deviation from ADR-CONT-044, as
|
|
99
|
+
// with UpdateOrgUnitRequest) so the app's add-role form and the backend
|
|
100
|
+
// validate against one published shape.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export const OpenRoleResponseSchema = z.object({ openRole: OpenRoleSchema });
|
|
104
|
+
|
|
105
|
+
export const OpenRoleListResponseSchema = z.object({
|
|
106
|
+
unitId: z.string().uuid(),
|
|
107
|
+
openRoles: z.array(OpenRoleSchema),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** ISO calendar date (YYYY-MM-DD) — version-agnostic vs `z.string().date()`. */
|
|
111
|
+
const IsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
112
|
+
message: "must be an ISO date (YYYY-MM-DD)",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const CreateOpenRoleRequestSchema = z.object({
|
|
116
|
+
title: z.string().min(1).max(255).nullable().optional(),
|
|
117
|
+
targetStartDate: IsoDateSchema.nullable().optional(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/** Only the non-terminal client transitions are accepted; `fill` is its own
|
|
121
|
+
* endpoint and `open` is the seeded initial state, never set by the client. */
|
|
122
|
+
export const UpdateOpenRoleStatusRequestSchema = z.object({
|
|
123
|
+
status: z.enum(["hiring", "closed"]),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fill an open role by seating an existing org member (`userId`) or inviting a
|
|
128
|
+
* new person by email. Exactly one branch — a discriminated union keyed on `mode`.
|
|
129
|
+
*/
|
|
130
|
+
export const FillOpenRoleRequestSchema = z.discriminatedUnion("mode", [
|
|
131
|
+
z.object({ mode: z.literal("existing"), userId: z.string().uuid() }),
|
|
132
|
+
z.object({
|
|
133
|
+
mode: z.literal("invite"),
|
|
134
|
+
email: z.string().email(),
|
|
135
|
+
role: z.enum(["admin", "member"]),
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Effective manager of an org unit — a user whose membership (direct or
|
|
141
|
+
* inherited from an ancestor) grants the `orgUnit.{id}.manage` capability.
|
|
142
|
+
*
|
|
143
|
+
* Direct grants have `inheritedFromUnitId === unitId`. For inherited grants,
|
|
144
|
+
* `inheritedFromUnitId` identifies the ancestor whose membership propagates
|
|
145
|
+
* authority down the subtree (ltree `path @>` containment).
|
|
146
|
+
*/
|
|
147
|
+
export const OrgUnitPermissionsEntrySchema = z.object({
|
|
148
|
+
userId: z.string().uuid(),
|
|
149
|
+
membershipRole: UnitMembershipRoleSchema,
|
|
150
|
+
inheritedFromUnitId: z.string().uuid(),
|
|
151
|
+
inheritedFromUnitName: z.string().min(1),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export const ListOrgUnitPermissionsResponseSchema = z.object({
|
|
155
|
+
unitId: z.string().uuid(),
|
|
156
|
+
entries: z.array(OrgUnitPermissionsEntrySchema),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// PATCH /api/org-units/:unitId (PRD-00526)
|
|
161
|
+
// Request schema colocated with the response schema so the app can import
|
|
162
|
+
// the same shape backend validates against. This is a narrow deviation from
|
|
163
|
+
// ADR-CONT-044 (request schemas normally live in backend) because the
|
|
164
|
+
// partial-update + at-least-one-field refinement is the published promise
|
|
165
|
+
// consumers must satisfy.
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
export const UpdateOrgUnitRequestSchema = z
|
|
169
|
+
.object({
|
|
170
|
+
name: z.string().min(1).max(255).optional(),
|
|
171
|
+
description: z.string().max(2000).nullable().optional(),
|
|
172
|
+
})
|
|
173
|
+
.refine((v) => v.name !== undefined || v.description !== undefined, {
|
|
174
|
+
message: "at least one of name or description must be provided",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const UpdateOrgUnitResponseSchema = z.object({ unit: OrgUnitSchema });
|
|
178
|
+
|
|
179
|
+
// --- Inferred types ---
|
|
180
|
+
|
|
181
|
+
export type OrgUnit = z.infer<typeof OrgUnitSchema>;
|
|
182
|
+
export type OrgUnitTreeNode = z.infer<typeof OrgUnitTreeNodeSchema>;
|
|
183
|
+
export type OrgUnitMembership = z.infer<typeof OrgUnitMembershipSchema>;
|
|
184
|
+
export type OrgUnitRelationship = z.infer<typeof OrgUnitRelationshipSchema>;
|
|
185
|
+
export type OrgLevelConfig = z.infer<typeof OrgLevelConfigSchema>;
|
|
186
|
+
export type OrgLevelIcon = z.infer<typeof OrgLevelIconSchema>;
|
|
187
|
+
export type OrgUnitResponse = z.infer<typeof OrgUnitResponseSchema>;
|
|
188
|
+
export type OrgUnitTreeResponse = z.infer<typeof OrgUnitTreeResponseSchema>;
|
|
189
|
+
export type MissingAtNextLevel = z.infer<typeof MissingAtNextLevelSchema>;
|
|
190
|
+
export type OrgUnitChildrenResponse = z.infer<
|
|
191
|
+
typeof OrgUnitChildrenResponseSchema
|
|
192
|
+
>;
|
|
193
|
+
export type OrgUnitAncestorsResponse = z.infer<
|
|
194
|
+
typeof OrgUnitAncestorsResponseSchema
|
|
195
|
+
>;
|
|
196
|
+
export type OrgUnitDescendantsResponse = z.infer<
|
|
197
|
+
typeof OrgUnitDescendantsResponseSchema
|
|
198
|
+
>;
|
|
199
|
+
export type OrgUnitRelationshipsResponse = z.infer<
|
|
200
|
+
typeof OrgUnitRelationshipsResponseSchema
|
|
201
|
+
>;
|
|
202
|
+
export type OrgLevelConfigResponse = z.infer<
|
|
203
|
+
typeof OrgLevelConfigResponseSchema
|
|
204
|
+
>;
|
|
205
|
+
export type OrgUnitMembershipResponse = z.infer<
|
|
206
|
+
typeof OrgUnitMembershipResponseSchema
|
|
207
|
+
>;
|
|
208
|
+
export type OrgUnitMembershipListResponse = z.infer<
|
|
209
|
+
typeof OrgUnitMembershipListResponseSchema
|
|
210
|
+
>;
|
|
211
|
+
export type OpenRole = z.infer<typeof OpenRoleSchema>;
|
|
212
|
+
export type OpenRoleResponse = z.infer<typeof OpenRoleResponseSchema>;
|
|
213
|
+
export type OpenRoleListResponse = z.infer<typeof OpenRoleListResponseSchema>;
|
|
214
|
+
export type CreateOpenRoleRequest = z.infer<typeof CreateOpenRoleRequestSchema>;
|
|
215
|
+
export type UpdateOpenRoleStatusRequest = z.infer<
|
|
216
|
+
typeof UpdateOpenRoleStatusRequestSchema
|
|
217
|
+
>;
|
|
218
|
+
export type FillOpenRoleRequest = z.infer<typeof FillOpenRoleRequestSchema>;
|
|
219
|
+
export type OrgUnitPermissionsEntry = z.infer<
|
|
220
|
+
typeof OrgUnitPermissionsEntrySchema
|
|
221
|
+
>;
|
|
222
|
+
export type ListOrgUnitPermissionsResponse = z.infer<
|
|
223
|
+
typeof ListOrgUnitPermissionsResponseSchema
|
|
224
|
+
>;
|
|
225
|
+
export type UpdateOrgUnitRequest = z.infer<typeof UpdateOrgUnitRequestSchema>;
|
|
226
|
+
export type UpdateOrgUnitResponse = z.infer<typeof UpdateOrgUnitResponseSchema>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org Unit Schemas
|
|
3
|
+
*
|
|
4
|
+
* The canonical org-unit model (ADR-BE-120): unit vocabulary enums, the unit and
|
|
5
|
+
* tree-node shapes, memberships and open roles.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `./schemas.ts`, which re-exports this module unchanged — import
|
|
8
|
+
* from `./schemas` (or the `./org` entry), not from this file directly.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import {
|
|
12
|
+
ORG_UNIT_ABSOLUTE_MAX_DEPTH,
|
|
13
|
+
UNIT_OWNER_MEMBERSHIP_ROLES,
|
|
14
|
+
} from "./org-depth";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// OrgUnit canonical model (ADR-BE-120)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const OrgUnitClassificationSchema = z.enum([
|
|
21
|
+
"execution_unit",
|
|
22
|
+
"org_container",
|
|
23
|
+
"leadership",
|
|
24
|
+
"custom",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export const OrgUnitSyncModeSchema = z.enum([
|
|
28
|
+
"manual_only",
|
|
29
|
+
"synced_readonly",
|
|
30
|
+
"synced_with_overrides",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export const OrgUnitVisibilitySchema = z.enum([
|
|
34
|
+
"public",
|
|
35
|
+
"private",
|
|
36
|
+
"restricted",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const OrgUnitRelationshipTypeSchema = z.enum([
|
|
40
|
+
"collaborates_with",
|
|
41
|
+
"reports_to",
|
|
42
|
+
"depends_on",
|
|
43
|
+
"custom",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export const OrgUnitRelationshipRoleSchema = z.enum([
|
|
47
|
+
"owner",
|
|
48
|
+
"participant",
|
|
49
|
+
"supporting",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Positional slot a member holds in the org-unit tree. ORTHOGONAL to the
|
|
54
|
+
* `OrgChartRole` policy vocabulary (this is an INPUT to authority resolution,
|
|
55
|
+
* not the derived policy role). Level is encoded in the value (l1…l20, one
|
|
56
|
+
* per depth up to `ORG_UNIT_ABSOLUTE_MAX_DEPTH`).
|
|
57
|
+
*/
|
|
58
|
+
export const UnitMembershipRoleSchema = z.enum([
|
|
59
|
+
"member",
|
|
60
|
+
...UNIT_OWNER_MEMBERSHIP_ROLES,
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export type UnitMembershipRole = z.infer<typeof UnitMembershipRoleSchema>;
|
|
64
|
+
|
|
65
|
+
export const OrgUnitMembershipStatusSchema = z.enum([
|
|
66
|
+
"active",
|
|
67
|
+
"pending",
|
|
68
|
+
"removed",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
export const OrgUnitMembershipSourceSchema = z.enum([
|
|
72
|
+
"manual",
|
|
73
|
+
"google_groups",
|
|
74
|
+
"scim",
|
|
75
|
+
"hris",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Hiring lifecycle of an open role. See `OpenRoleStatus` in `./org-units` for
|
|
80
|
+
* the transition rules. Only `open` and `hiring` are "active" (render a card
|
|
81
|
+
* and count toward `openRoleCount`); `filled`/`closed` are terminal.
|
|
82
|
+
*/
|
|
83
|
+
export const OpenRoleStatusSchema = z.enum([
|
|
84
|
+
"open",
|
|
85
|
+
"hiring",
|
|
86
|
+
"filled",
|
|
87
|
+
"closed",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
export const OrgUnitErrorCodeSchema = z.enum([
|
|
91
|
+
"CYCLE_BLOCKED",
|
|
92
|
+
"DEPTH_EXCEEDED",
|
|
93
|
+
"TARGET_ARCHIVED",
|
|
94
|
+
"ROOT_HAS_NO_PARENT",
|
|
95
|
+
"PARENT_ARCHIVED",
|
|
96
|
+
"SLUG_TAKEN",
|
|
97
|
+
"CROSS_ORG_REPARENT",
|
|
98
|
+
"ORDERKEY_CONFLICT",
|
|
99
|
+
"SIBLING_NOT_FOUND",
|
|
100
|
+
"SIBLINGS_DIFFERENT_PARENT",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
export const OrgUnitSchema = z.object({
|
|
104
|
+
id: z.string().uuid(),
|
|
105
|
+
orgId: z.string().uuid(),
|
|
106
|
+
parentId: z.string().uuid().nullable(),
|
|
107
|
+
slug: z.string().min(1),
|
|
108
|
+
name: z.string().min(1),
|
|
109
|
+
description: z.string().nullable(),
|
|
110
|
+
typeTag: z.string().min(1),
|
|
111
|
+
classification: OrgUnitClassificationSchema.nullable(),
|
|
112
|
+
orderKey: z.string().min(1),
|
|
113
|
+
path: z.string().min(1),
|
|
114
|
+
syncMode: OrgUnitSyncModeSchema,
|
|
115
|
+
visibilityDefault: OrgUnitVisibilitySchema.nullable(),
|
|
116
|
+
metadata: z.record(z.string(), z.unknown()),
|
|
117
|
+
archivedAt: z.string().nullable(),
|
|
118
|
+
createdAt: z.string(),
|
|
119
|
+
updatedAt: z.string(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Active users home directly at this unit who have no level-(depth+1) home
|
|
124
|
+
* assignment within its subtree. The settings UI surfaces this as a
|
|
125
|
+
* synthetic "[child level label] assignment missing" row above the
|
|
126
|
+
* children list at every level. `null` for leaf-level units (no children
|
|
127
|
+
* configured below them) where the concept is meaningless.
|
|
128
|
+
*/
|
|
129
|
+
export const MissingAtNextLevelSchema = z.object({
|
|
130
|
+
count: z.number().int().min(0),
|
|
131
|
+
userIds: z.array(z.string().uuid()),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
|
|
135
|
+
depth: z.number().int().min(1).max(ORG_UNIT_ABSOLUTE_MAX_DEPTH),
|
|
136
|
+
hasChildren: z.boolean(),
|
|
137
|
+
/**
|
|
138
|
+
* WHAT it counts is the `.describe()` below — the single field-level
|
|
139
|
+
* description, which reaches `openapi/backend.yaml` and the generated API
|
|
140
|
+
* types. This comment carries only WHY, so the two cannot drift into
|
|
141
|
+
* disagreeing.
|
|
142
|
+
*
|
|
143
|
+
* Why accounts: it is the right population for anything that needs an account
|
|
144
|
+
* to act on — the share dialog's fanout, the unit-scoped members table. It is
|
|
145
|
+
* backed by `home_assignments`, whose `user_id` is NOT NULL, so an accountless
|
|
146
|
+
* person structurally cannot appear in it. Deliberately NOT redefined and NOT
|
|
147
|
+
* deprecated; {@link placedPeopleCount} answers the headcount question, and the
|
|
148
|
+
* two legitimately disagree (ADR-CONTRACTS-150, ADR-BE-657).
|
|
149
|
+
*/
|
|
150
|
+
memberCount: z
|
|
151
|
+
.number()
|
|
152
|
+
.int()
|
|
153
|
+
.min(0)
|
|
154
|
+
.describe(
|
|
155
|
+
"Accounts with a home assignment, rolled up over this unit's subtree. Legitimately 0 for a fully staffed department in an HRIS-imported org, where almost nobody has signed in — use placedPeopleCount for headcount.",
|
|
156
|
+
),
|
|
157
|
+
/** Drives the "(N + M open)" count split in the people surfaces. */
|
|
158
|
+
openRoleCount: z
|
|
159
|
+
.number()
|
|
160
|
+
.int()
|
|
161
|
+
.min(0)
|
|
162
|
+
.describe(
|
|
163
|
+
"Active open roles (`open` + `hiring`) rolled up over this unit's subtree. Vacant seats only — an HRIS-imported seat is `lifecycle_owner='import:hris'` and is counted by neither this nor memberCount.",
|
|
164
|
+
),
|
|
165
|
+
/**
|
|
166
|
+
* Predicate: `placement_role='canonical'`, `status <> 'ended'`, keyed by
|
|
167
|
+
* `positions.unit_id`. Placement and home are independent graphs
|
|
168
|
+
* (ADR-CTRL-159), so a person seated here may be homed elsewhere and vice
|
|
169
|
+
* versa. The `positions.status` non-filter stated in the `.describe()` below
|
|
170
|
+
* is deliberate and load-bearing — the backend pins it with a test whose name
|
|
171
|
+
* says so (ADR-BE-657).
|
|
172
|
+
*/
|
|
173
|
+
placedPeopleCount: z
|
|
174
|
+
.number()
|
|
175
|
+
.int()
|
|
176
|
+
.min(0)
|
|
177
|
+
.describe(
|
|
178
|
+
"Distinct people holding a live canonical seat in this unit's subtree, whether or not they have an account. Deliberately does NOT filter positions.status: an occupied `closed` seat still draws a card on the org chart, so a filter here would disagree with it.",
|
|
179
|
+
),
|
|
180
|
+
missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export const OrgUnitMembershipSchema = z.object({
|
|
184
|
+
id: z.string().uuid(),
|
|
185
|
+
orgId: z.string().uuid(),
|
|
186
|
+
unitId: z.string().uuid(),
|
|
187
|
+
userId: z.string().uuid(),
|
|
188
|
+
membershipRole: UnitMembershipRoleSchema,
|
|
189
|
+
status: OrgUnitMembershipStatusSchema,
|
|
190
|
+
source: OrgUnitMembershipSourceSchema,
|
|
191
|
+
sourceRef: z.string().nullable(),
|
|
192
|
+
createdAt: z.string(),
|
|
193
|
+
updatedAt: z.string(),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* An open role — a persisted placeholder for the person who will sit in a unit.
|
|
198
|
+
* Carries unit membership (a seat) but never authority. `title` and
|
|
199
|
+
* `targetStartDate` are optional planning attributes. On `fill`, `filledByUserId`
|
|
200
|
+
* points at the seated user and a real `org_unit_membership` is created; the row
|
|
201
|
+
* is kept as staffing history. `targetStartDate` is an ISO date (YYYY-MM-DD).
|
|
202
|
+
*/
|
|
203
|
+
export const OpenRoleSchema = z.object({
|
|
204
|
+
id: z.string().uuid(),
|
|
205
|
+
orgId: z.string().uuid(),
|
|
206
|
+
unitId: z.string().uuid(),
|
|
207
|
+
title: z.string().nullable(),
|
|
208
|
+
targetStartDate: z.string().nullable(),
|
|
209
|
+
status: OpenRoleStatusSchema,
|
|
210
|
+
filledByUserId: z.string().uuid().nullable(),
|
|
211
|
+
// The open role's own reporting edge (ADR-BE-291): defaults to its creator's
|
|
212
|
+
// seat, editable/clearable. Drives chart placement now that the unit-head
|
|
213
|
+
// anchor is retired (ADR-BE-292). Null → unmanaged (Unassigned bucket).
|
|
214
|
+
//
|
|
215
|
+
// The manager POSITION, not a user (ADR-CONTRACTS-131). The retired
|
|
216
|
+
// user-keyed flattening was a lie in two directions: an open-role manager
|
|
217
|
+
// already surfaced as null because it had no user to collapse to, and
|
|
218
|
+
// post-HRIS every open role reporting to an imported (accountless) manager
|
|
219
|
+
// would read null too.
|
|
220
|
+
reportsToPositionId: z.string().uuid().nullable(),
|
|
221
|
+
createdAt: z.string(),
|
|
222
|
+
updatedAt: z.string(),
|
|
223
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership Transfer DTOs (PRD-00153)
|
|
3
|
+
*
|
|
4
|
+
* Request, status and acceptance-view shapes for the ownership-transfer UI flows,
|
|
5
|
+
* plus the acceptance-page responsibility checklist.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `./types.ts`, which re-exports this module unchanged — import
|
|
8
|
+
* from `./types` (or the `./org` entry), not from this file directly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Ownership Transfer DTOs (PRD-00153)
|
|
13
|
+
// Rich types for the transfer UI flows, supplementing OwnershipTransferRequest
|
|
14
|
+
// and OwnershipTransferStatus in `./types`.
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/** Request body for initiating ownership transfer (extends existing to include optional note). */
|
|
18
|
+
export interface TransferInitiateRequest {
|
|
19
|
+
readonly newOwnerEmail: string;
|
|
20
|
+
readonly note?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Response for transfer status queries — extends existing OwnershipTransferStatus. */
|
|
24
|
+
export interface TransferStatusResponse {
|
|
25
|
+
readonly pending: boolean;
|
|
26
|
+
readonly newOwnerEmail?: string;
|
|
27
|
+
readonly note?: string;
|
|
28
|
+
readonly requestedAt?: string;
|
|
29
|
+
readonly expiresAt?: string;
|
|
30
|
+
readonly initiatorName?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read-only view for the transfer acceptance page. */
|
|
34
|
+
export interface TransferAcceptanceView {
|
|
35
|
+
readonly workspaceName: string;
|
|
36
|
+
readonly initiatorName: string;
|
|
37
|
+
readonly initiatorEmail: string;
|
|
38
|
+
readonly note?: string;
|
|
39
|
+
readonly expiresAt: string;
|
|
40
|
+
readonly token: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Responsibility checklist items shown on the acceptance page. */
|
|
44
|
+
export const TRANSFER_RESPONSIBILITIES = [
|
|
45
|
+
"You will become the sole owner of this workspace",
|
|
46
|
+
"You will be responsible for billing and compliance",
|
|
47
|
+
"The current owner will be downgraded to admin",
|
|
48
|
+
"This action cannot be undone without a new transfer",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
export type TransferResponsibility = (typeof TRANSFER_RESPONSIBILITIES)[number];
|