@company-semantics/contracts 58.5.1 → 59.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 +8 -3
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +3 -3
- package/src/decisions/README.md +41 -0
- package/src/decisions/__tests__/README.md +37 -0
- package/src/decisions/__tests__/schemas.test.ts +146 -0
- package/src/decisions/index.ts +32 -0
- package/src/decisions/schemas.ts +107 -0
- package/src/decisions/types.ts +192 -0
- package/src/execution/__tests__/registry.test.ts +21 -0
- package/src/execution/kinds.ts +4 -0
- package/src/execution/registry.ts +23 -0
- package/src/index.ts +6 -0
- package/src/integrations/README.md +2 -0
- package/src/integrations/index.ts +10 -2
- package/src/integrations/schemas.ts +87 -0
- package/src/message-parts/__tests__/confirmation.test.ts +1 -0
- package/src/message-parts/confirmation.ts +1 -0
- package/src/org/README.md +3 -1
- package/src/org/__tests__/structure-inference.test.ts +70 -2
- package/src/org/index.ts +1 -0
- package/src/org/structure-inference.ts +36 -16
- package/src/resource-key-tables.ts +191 -0
- package/src/resource-keys.ts +12 -176
|
@@ -76,6 +76,29 @@ export const EXECUTION_KINDS = {
|
|
|
76
76
|
templateId: "integration.disconnect",
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
|
+
"integration.purge": {
|
|
80
|
+
kind: "integration.purge",
|
|
81
|
+
domain: "integration",
|
|
82
|
+
display: {
|
|
83
|
+
label: "Purge integration data",
|
|
84
|
+
pastTenseLabel: "Integration data purged",
|
|
85
|
+
icon: "unlink",
|
|
86
|
+
},
|
|
87
|
+
governance: {
|
|
88
|
+
visibility: "admin",
|
|
89
|
+
requiresAdmin: true,
|
|
90
|
+
// Irreversible by design: the unwind deletes source-owned facts and
|
|
91
|
+
// history, so no kind reverses this one (reversibleBy stays unset).
|
|
92
|
+
},
|
|
93
|
+
ui: {
|
|
94
|
+
showInAdmin: true,
|
|
95
|
+
showInTimeline: true,
|
|
96
|
+
confirmBeforeRun: true,
|
|
97
|
+
},
|
|
98
|
+
explanation: {
|
|
99
|
+
templateId: "integration.purge",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
79
102
|
"profile.update": {
|
|
80
103
|
kind: "profile.update",
|
|
81
104
|
domain: "profile",
|
package/src/index.ts
CHANGED
|
@@ -1376,3 +1376,9 @@ export * from "./ingestion";
|
|
|
1376
1376
|
// HrConnectionStatus / HrConnectInput — see ./integrations/schemas.ts.
|
|
1377
1377
|
// Pairs with the `HR` member of INTEGRATION_CATEGORIES above.
|
|
1378
1378
|
export * from "./integrations";
|
|
1379
|
+
|
|
1380
|
+
// Decision vocabulary (ADR-CONTRACTS-147 / ADR-BE-639)
|
|
1381
|
+
// A question the system asks a human, and what answering it DOES. Display and
|
|
1382
|
+
// effect are separate channels: an option's `label` is prose for a reader, its
|
|
1383
|
+
// `effect` is the only thing an apply may read. See ./decisions/types.ts.
|
|
1384
|
+
export * from "./decisions";
|
|
@@ -36,6 +36,8 @@ alongside `Meetings`, `Comms`, and `Docs`.
|
|
|
36
36
|
- `HrConnectInputSchema` — Input submitted by the app to initiate an HR provider connection.
|
|
37
37
|
- `HrConnectionStatus` _(type)_
|
|
38
38
|
- `HrConnectionStatusSchema` — Health of the most recent HR provider sync.
|
|
39
|
+
- `HrisPurgeImpact` _(type)_
|
|
40
|
+
- `HrisPurgeImpactSchema` — Impact of a break-glass HRIS purge (`integration.purge`), counted per entity.
|
|
39
41
|
|
|
40
42
|
<!-- END GENERATED: readme-public-api -->
|
|
41
43
|
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @see ./schemas.ts for the full schema definitions and invariants.
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
HrConnectionStatusSchema,
|
|
8
|
+
HrConnectInputSchema,
|
|
9
|
+
HrisPurgeImpactSchema,
|
|
10
|
+
} from "./schemas";
|
|
7
11
|
|
|
8
|
-
export type {
|
|
12
|
+
export type {
|
|
13
|
+
HrConnectionStatus,
|
|
14
|
+
HrConnectInput,
|
|
15
|
+
HrisPurgeImpact,
|
|
16
|
+
} from "./schemas";
|
|
@@ -146,3 +146,90 @@ export const HrConnectInputSchema = z
|
|
|
146
146
|
description: "Input to initiate an HR provider connection.",
|
|
147
147
|
});
|
|
148
148
|
export type HrConnectInput = z.infer<typeof HrConnectInputSchema>;
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// HRIS purge impact
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Impact of a break-glass HRIS purge (`integration.purge`), counted per
|
|
156
|
+
* entity. One shape serves four readers: the purge-preview response, the
|
|
157
|
+
* execution result, the `integration.purged` fact details, and the
|
|
158
|
+
* `connection.purged` audit metadata. Preview and purge are built from the
|
|
159
|
+
* same predicate builders, so on an unchanged org the previewed counts and
|
|
160
|
+
* the executed counts are equal by construction.
|
|
161
|
+
*/
|
|
162
|
+
export const HrisPurgeImpactSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
peopleUnlinked: z.number().int().min(0).meta({
|
|
165
|
+
description: "Provider identity links deleted (people unlinked).",
|
|
166
|
+
}),
|
|
167
|
+
personsRetained: z
|
|
168
|
+
.number()
|
|
169
|
+
.int()
|
|
170
|
+
.min(0)
|
|
171
|
+
.meta({
|
|
172
|
+
description:
|
|
173
|
+
"Persons left in the graph — always all of them; persons are " +
|
|
174
|
+
"never deleted by a purge, only unlinked from the provider.",
|
|
175
|
+
}),
|
|
176
|
+
unitsArchived: z.number().int().min(0).meta({
|
|
177
|
+
description: "Source-owned org units soft-archived.",
|
|
178
|
+
}),
|
|
179
|
+
unitsRetained: z.number().int().min(0).meta({
|
|
180
|
+
description:
|
|
181
|
+
"Provider-imported org units retained because a user claimed them.",
|
|
182
|
+
}),
|
|
183
|
+
positionsClosed: z.number().int().min(0).meta({
|
|
184
|
+
description: "Source-owned positions closed (never deleted).",
|
|
185
|
+
}),
|
|
186
|
+
positionsRetained: z.number().int().min(0).meta({
|
|
187
|
+
description:
|
|
188
|
+
"Provider-imported positions retained because a user claimed them.",
|
|
189
|
+
}),
|
|
190
|
+
occupanciesEnded: z.number().int().min(0).meta({
|
|
191
|
+
description: "Source-owned occupancies ended (employment history kept).",
|
|
192
|
+
}),
|
|
193
|
+
occupanciesRetained: z.number().int().min(0).meta({
|
|
194
|
+
description:
|
|
195
|
+
"Provider-imported occupancies retained because a user claimed them.",
|
|
196
|
+
}),
|
|
197
|
+
reportingEdgesRemoved: z.number().int().min(0).meta({
|
|
198
|
+
description: "Source-owned reporting edges deleted.",
|
|
199
|
+
}),
|
|
200
|
+
reportingEdgesRetained: z
|
|
201
|
+
.number()
|
|
202
|
+
.int()
|
|
203
|
+
.min(0)
|
|
204
|
+
.meta({
|
|
205
|
+
description:
|
|
206
|
+
"Provider-imported reporting edges retained because a user took " +
|
|
207
|
+
"ownership of them (user_override or human_confirmed authority).",
|
|
208
|
+
}),
|
|
209
|
+
syncRunsDeleted: z.number().int().min(0).meta({
|
|
210
|
+
description: "Sync-run history rows deleted.",
|
|
211
|
+
}),
|
|
212
|
+
conflictsDeleted: z.number().int().min(0).meta({
|
|
213
|
+
description: "Conflict-ledger rows deleted.",
|
|
214
|
+
}),
|
|
215
|
+
acknowledgementsDeleted: z.number().int().min(0).meta({
|
|
216
|
+
description: "Divergence acknowledgements deleted.",
|
|
217
|
+
}),
|
|
218
|
+
sourceUnitMappingsDeleted: z.number().int().min(0).meta({
|
|
219
|
+
description: "Source-unit mapping rows deleted.",
|
|
220
|
+
}),
|
|
221
|
+
authorityRowsDeleted: z
|
|
222
|
+
.number()
|
|
223
|
+
.int()
|
|
224
|
+
.min(0)
|
|
225
|
+
.meta({
|
|
226
|
+
description:
|
|
227
|
+
"source-authority rows deleted — only rows whose authority is the " +
|
|
228
|
+
"provider itself; human-confirmed and user-override rows are never " +
|
|
229
|
+
"touched.",
|
|
230
|
+
}),
|
|
231
|
+
})
|
|
232
|
+
.meta({
|
|
233
|
+
description: "Per-entity impact of a break-glass HRIS purge.",
|
|
234
|
+
});
|
|
235
|
+
export type HrisPurgeImpact = z.infer<typeof HrisPurgeImpactSchema>;
|
|
@@ -114,6 +114,7 @@ describe("CONFIRMATION_LABELS", () => {
|
|
|
114
114
|
expect(CONFIRMATION_LABELS).toStrictEqual({
|
|
115
115
|
"integration.connect": "Connect Integration",
|
|
116
116
|
"integration.disconnect": "Disconnect Integration",
|
|
117
|
+
"integration.purge": "Purge Integration Data",
|
|
117
118
|
"profile.update": "Update Profile",
|
|
118
119
|
"slack.send": "Send Slack Message",
|
|
119
120
|
"data.ingest": "Import Channel",
|
|
@@ -39,6 +39,7 @@ export type ConfirmationRiskLevel =
|
|
|
39
39
|
export const CONFIRMATION_LABELS: Record<ExecutionKind, string> = {
|
|
40
40
|
"integration.connect": "Connect Integration",
|
|
41
41
|
"integration.disconnect": "Disconnect Integration",
|
|
42
|
+
"integration.purge": "Purge Integration Data",
|
|
42
43
|
"profile.update": "Update Profile",
|
|
43
44
|
"slack.send": "Send Slack Message",
|
|
44
45
|
"data.ingest": "Import Channel",
|
package/src/org/README.md
CHANGED
|
@@ -340,6 +340,7 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
340
340
|
- `RoleCatalogEntry` _(type)_ — Entry in the RBAC roles catalog (GET /api/rbac/roles).
|
|
341
341
|
- `RoleCatalogEntrySchema`
|
|
342
342
|
- `RoleCatalogResponseSchema`
|
|
343
|
+
- `STRUCTURE_REVIEW_ITEM_KINDS` — A question the engine is asking a human, with the competing signals attached.
|
|
343
344
|
- `ScopeCheckBatchResponse` _(type)_
|
|
344
345
|
- `ScopeCheckBatchResponseSchema`
|
|
345
346
|
- `ScopeCheckResponse` _(type)_
|
|
@@ -385,7 +386,7 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
385
386
|
- `StructureReportingFact` _(type)_
|
|
386
387
|
- `StructureReportingFactSchema` — One reporting edge, at person granularity.
|
|
387
388
|
- `StructureReviewItem` _(type)_
|
|
388
|
-
- `StructureReviewItemSchema` — A
|
|
389
|
+
- `StructureReviewItemSchema` — A review item IS a {@link DecisionQuestion}, plus the addressing that says what the question is about.
|
|
389
390
|
- `StructureUnitAuthority` _(type)_
|
|
390
391
|
- `StructureUnitAuthoritySchema` — How much license the engine has over an EXISTING unit or placement.
|
|
391
392
|
- `SubmitInteractiveTaskResponse` _(type)_
|
|
@@ -474,6 +475,7 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
474
475
|
**Internal domains:**
|
|
475
476
|
|
|
476
477
|
- `api`
|
|
478
|
+
- `decisions`
|
|
477
479
|
- `identity`
|
|
478
480
|
- `permissions`
|
|
479
481
|
|
|
@@ -218,11 +218,34 @@ describe("PersonStructureOutcomeSchema", () => {
|
|
|
218
218
|
describe("StructureReviewItemSchema", () => {
|
|
219
219
|
const base = {
|
|
220
220
|
kind: "placement_conflict",
|
|
221
|
+
id: "placement_conflict:clarissa",
|
|
222
|
+
header: "Placement",
|
|
221
223
|
personIds: ["clarissa"],
|
|
222
224
|
unitTempIds: ["u1", "u2"],
|
|
223
225
|
question: "Which unit does this Account Executive belong to?",
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
options: [
|
|
227
|
+
{
|
|
228
|
+
id: "u2",
|
|
229
|
+
label: "Sales — matching the role family the other 11 AEs sit in",
|
|
230
|
+
effect: {
|
|
231
|
+
kind: "place_people",
|
|
232
|
+
personIds: ["clarissa"],
|
|
233
|
+
unitTempId: "u2",
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "u1",
|
|
238
|
+
label: "Marketing — matching the manager she reports to",
|
|
239
|
+
effect: {
|
|
240
|
+
kind: "place_people",
|
|
241
|
+
personIds: ["clarissa"],
|
|
242
|
+
unitTempId: "u1",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
recommendedOptionId: "u2",
|
|
247
|
+
multiSelect: false,
|
|
248
|
+
freeFormAllowed: false,
|
|
226
249
|
signals: {
|
|
227
250
|
department: "Human Resources",
|
|
228
251
|
manager: "VP of Marketing",
|
|
@@ -243,6 +266,51 @@ describe("StructureReviewItemSchema", () => {
|
|
|
243
266
|
StructureReviewItemSchema.parse({ ...base, kind: "something_else" }),
|
|
244
267
|
).toThrow();
|
|
245
268
|
});
|
|
269
|
+
|
|
270
|
+
it("accepts the three rule-derived kinds the backend attaches after the gate", () => {
|
|
271
|
+
for (const kind of [
|
|
272
|
+
"reporting_anomaly",
|
|
273
|
+
"unit_adoption",
|
|
274
|
+
"unit_retirement",
|
|
275
|
+
]) {
|
|
276
|
+
expect(() =>
|
|
277
|
+
StructureReviewItemSchema.parse({ ...base, kind }),
|
|
278
|
+
).not.toThrow();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("inherits the decision rules — a recommendation must name a real option", () => {
|
|
283
|
+
expect(() =>
|
|
284
|
+
StructureReviewItemSchema.parse({ ...base, recommendedOptionId: "u9" }),
|
|
285
|
+
).toThrow();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("carries the effect separately from the label, so an apply never reads prose", () => {
|
|
289
|
+
const parsed = StructureReviewItemSchema.parse({
|
|
290
|
+
...base,
|
|
291
|
+
kind: "unit_name",
|
|
292
|
+
options: [
|
|
293
|
+
{
|
|
294
|
+
id: "keep",
|
|
295
|
+
label: "Keep the name 'Human Resources'.",
|
|
296
|
+
effect: { kind: "none" },
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "rename",
|
|
300
|
+
label:
|
|
301
|
+
"Rename the existing unit to 'People' (same unitId, preserving history)",
|
|
302
|
+
effect: { kind: "rename_unit", unitTempId: "u1", name: "People" },
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
recommendedOptionId: "keep",
|
|
306
|
+
});
|
|
307
|
+
const rename = parsed.options.find((option) => option.id === "rename");
|
|
308
|
+
expect(rename?.effect).toEqual({
|
|
309
|
+
kind: "rename_unit",
|
|
310
|
+
unitTempId: "u1",
|
|
311
|
+
name: "People",
|
|
312
|
+
});
|
|
313
|
+
});
|
|
246
314
|
});
|
|
247
315
|
|
|
248
316
|
describe("StructureProposalSchema", () => {
|
package/src/org/index.ts
CHANGED
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
import { z } from "zod";
|
|
70
70
|
|
|
71
71
|
import { PositionReportingRelationshipTypeSchema } from "./position-reporting";
|
|
72
|
+
import {
|
|
73
|
+
DecisionQuestionBaseSchema,
|
|
74
|
+
refineDecisionQuestion,
|
|
75
|
+
} from "../decisions/schemas";
|
|
72
76
|
|
|
73
77
|
// ---------------------------------------------------------------------------
|
|
74
78
|
// StructureEvidence — typed support for a boundary, a name or a placement
|
|
@@ -378,28 +382,44 @@ export type PersonStructureOutcome = z.infer<
|
|
|
378
382
|
* never asks scores well on placement accuracy while being WORSE for the
|
|
379
383
|
* product than one that reports three signals disagreeing.
|
|
380
384
|
*/
|
|
381
|
-
export const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
export const STRUCTURE_REVIEW_ITEM_KINDS = [
|
|
386
|
+
"placement_conflict",
|
|
387
|
+
"unit_boundary",
|
|
388
|
+
"unit_name",
|
|
389
|
+
"unit_head",
|
|
390
|
+
"reporting_anomaly",
|
|
391
|
+
"unit_adoption",
|
|
392
|
+
"unit_retirement",
|
|
393
|
+
] as const;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* A review item IS a {@link DecisionQuestion}, plus the addressing that says
|
|
397
|
+
* what the question is about.
|
|
398
|
+
*
|
|
399
|
+
* It used to carry `recommended: string` and `alternatives: string[]` instead of
|
|
400
|
+
* options — three strings where the option text was simultaneously the label a
|
|
401
|
+
* reviewer read, the value on the receipt, and the payload the apply consumed.
|
|
402
|
+
* A unit was renamed to the literal sentence
|
|
403
|
+
* `Rename the existing unit to 'People' (same unitId, preserving history)`
|
|
404
|
+
* because of it. `options[].effect` is the channel that replaced the third job;
|
|
405
|
+
* see `../decisions/types.ts`.
|
|
406
|
+
*/
|
|
407
|
+
export const StructureReviewItemSchema = DecisionQuestionBaseSchema.extend({
|
|
408
|
+
kind: z.enum(STRUCTURE_REVIEW_ITEM_KINDS),
|
|
388
409
|
/** Opaque person ids this question is about. */
|
|
389
410
|
personIds: z.array(z.string()),
|
|
390
411
|
/** Proposal-scoped unit handles this question is about. */
|
|
391
412
|
unitTempIds: z.array(z.string()),
|
|
392
|
-
/**
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
413
|
+
/**
|
|
414
|
+
* DURABLE `org_units.id`s the question is about — a different address space
|
|
415
|
+
* from {@link StructureReviewItemSchema.shape.unitTempIds}, which is
|
|
416
|
+
* proposal-scoped. Needed because reconciliation asks about units that already
|
|
417
|
+
* exist and which the proposal may not contain at all.
|
|
418
|
+
*/
|
|
419
|
+
unitIds: z.array(z.string()).optional(),
|
|
398
420
|
/** The competing signals, so a reviewer can see WHY it is ambiguous. */
|
|
399
|
-
signals: z.record(z.string(), z.string()),
|
|
400
|
-
confidence: z.number().min(0).max(1),
|
|
401
421
|
evidence: z.array(StructureEvidenceSchema),
|
|
402
|
-
});
|
|
422
|
+
}).superRefine(refineDecisionQuestion);
|
|
403
423
|
export type StructureReviewItem = z.infer<typeof StructureReviewItemSchema>;
|
|
404
424
|
|
|
405
425
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fromQueryKey's routing tables, and the drift detector over them.
|
|
3
|
+
*
|
|
4
|
+
* Data, not operations: `resource-keys.ts` owns the round trip and reads these.
|
|
5
|
+
* Split out of that module, which had reached its size envelope — the seam is the
|
|
6
|
+
* one its own docblock already names (vocabulary in `resource-key-types.ts`,
|
|
7
|
+
* operations in `resource-keys.ts`, and these tables which are neither).
|
|
8
|
+
* See ADR-CONTRACTS-145.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ResourceKey } from "./resource-key-types";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
* fromQueryKey's routing tables, and the drift detector over them.
|
|
16
|
+
* ---------------------------------------------------------------------------
|
|
17
|
+
*
|
|
18
|
+
* `toQueryKey` has always been safe: its switch is exhaustive and its `never`
|
|
19
|
+
* guard makes a new union member a compile error. `fromQueryKey` was not. Its
|
|
20
|
+
* five lookup tables are hand-maintained, and a member missing from all of them
|
|
21
|
+
* fell through to a RUNTIME `throw` — which `matchesResourceKey` catches and
|
|
22
|
+
* turns into "matches nothing", and which `useResource` turns into a query that
|
|
23
|
+
* never fetches. Silent in both directions (ADR-CONTRACTS-119).
|
|
24
|
+
*
|
|
25
|
+
* Two mechanisms, and BOTH are needed:
|
|
26
|
+
*
|
|
27
|
+
* `satisfies` on each table catches a WRONG entry — a typo, or a literal
|
|
28
|
+
* parked under the wrong scope. It cannot catch an entry that is simply
|
|
29
|
+
* absent, because an array is free to be a subset of its element type.
|
|
30
|
+
*
|
|
31
|
+
* `_EveryResourceKeyIsRouted` below catches the ABSENT entry, which is the
|
|
32
|
+
* failure that actually shipped. It is the half that closes the hole.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The identity segments a member carries — every field that is neither the type
|
|
37
|
+
* tag nor a scope discriminator. `member` → `"memberId"`; `commentThreads` →
|
|
38
|
+
* `"subjectType" | "subjectId"`; a scope-only key → `never`.
|
|
39
|
+
*/
|
|
40
|
+
type IdentityFieldsOf<T extends ResourceKey["type"]> = Exclude<
|
|
41
|
+
keyof Extract<ResourceKey, { type: T }>,
|
|
42
|
+
"type" | "orgId" | "userId" | "scope"
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Type literals whose whole shape is the tag plus one scope discriminator —
|
|
47
|
+
* i.e. the members that belong in a scope ARRAY rather than an identity MAP.
|
|
48
|
+
* Distributes over the union (`K` is a naked type parameter).
|
|
49
|
+
*/
|
|
50
|
+
type ScopeOnlyType<K = ResourceKey> = K extends {
|
|
51
|
+
type: infer T extends ResourceKey["type"];
|
|
52
|
+
}
|
|
53
|
+
? [IdentityFieldsOf<T>] extends [never]
|
|
54
|
+
? T
|
|
55
|
+
: never
|
|
56
|
+
: never;
|
|
57
|
+
|
|
58
|
+
type OrgScopedType = Extract<
|
|
59
|
+
ScopeOnlyType,
|
|
60
|
+
Extract<ResourceKey, { orgId: string }>["type"]
|
|
61
|
+
>;
|
|
62
|
+
type UserScopedType = Extract<
|
|
63
|
+
ScopeOnlyType,
|
|
64
|
+
Extract<ResourceKey, { userId: string }>["type"]
|
|
65
|
+
>;
|
|
66
|
+
type SystemScopedType = Extract<
|
|
67
|
+
ScopeOnlyType,
|
|
68
|
+
Extract<ResourceKey, { scope: "system" }>["type"]
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Identities with a single extra segment. The `satisfies` checks the FIELD NAME
|
|
73
|
+
* against that member's own keys, so renaming `memberId` in the union — or
|
|
74
|
+
* pointing a key at a field it does not have — fails to compile here.
|
|
75
|
+
*/
|
|
76
|
+
export const IDENTITY_FIELDS = {
|
|
77
|
+
member: "memberId",
|
|
78
|
+
team: "teamId",
|
|
79
|
+
department: "departmentId",
|
|
80
|
+
chat: "chatId",
|
|
81
|
+
companyMdDoc: "slug",
|
|
82
|
+
companyMdContextBank: "slug",
|
|
83
|
+
companyMdAccessRequests: "docId",
|
|
84
|
+
companyMdDocHistory: "docId",
|
|
85
|
+
orgUnit: "unitId",
|
|
86
|
+
orgUnitChildren: "unitId",
|
|
87
|
+
orgUnitAncestors: "unitId",
|
|
88
|
+
orgUnitMemberships: "unitId",
|
|
89
|
+
orgUnitPermissions: "unitId",
|
|
90
|
+
orgUnitOpenRoles: "unitId",
|
|
91
|
+
orgUnitMyAuthority: "unitId",
|
|
92
|
+
} as const satisfies { [T in ResourceKey["type"]]?: IdentityFieldsOf<T> };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Identities with a COMPOSITE (multi-segment) identity. Checked before
|
|
96
|
+
* {@link IDENTITY_FIELDS}, which hard-asserts a two-element `rest` and would
|
|
97
|
+
* otherwise reject these.
|
|
98
|
+
*
|
|
99
|
+
* A map of its own rather than widening `IDENTITY_FIELDS` to `string | string[]`:
|
|
100
|
+
* the single-segment case is every other key in the union, and making it pay for
|
|
101
|
+
* this one would put a branch in the hot path for no reader's benefit.
|
|
102
|
+
*/
|
|
103
|
+
export const COMPOSITE_IDENTITY_FIELDS = {
|
|
104
|
+
commentThreads: ["subjectType", "subjectId"],
|
|
105
|
+
companyMdDocVersion: ["docId", "versionId"],
|
|
106
|
+
} as const satisfies {
|
|
107
|
+
[T in ResourceKey["type"]]?: readonly IdentityFieldsOf<T>[];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const ORG_SCOPED_TYPES = [
|
|
111
|
+
"members",
|
|
112
|
+
"departments",
|
|
113
|
+
"chats",
|
|
114
|
+
"teams",
|
|
115
|
+
"integrations",
|
|
116
|
+
"invites",
|
|
117
|
+
"orgDirectory",
|
|
118
|
+
"auditEvents",
|
|
119
|
+
"timeline",
|
|
120
|
+
"workspace",
|
|
121
|
+
"workspaceDomains",
|
|
122
|
+
"authSettings",
|
|
123
|
+
"billing",
|
|
124
|
+
"aiUsage",
|
|
125
|
+
"deletionEligibility",
|
|
126
|
+
"transferOwnership",
|
|
127
|
+
"companyMdDocs",
|
|
128
|
+
"directGrants",
|
|
129
|
+
"orgTree",
|
|
130
|
+
"orgLevelConfig",
|
|
131
|
+
"peopleOrgChart",
|
|
132
|
+
"orgUnitOwners",
|
|
133
|
+
"actionItems",
|
|
134
|
+
"feed",
|
|
135
|
+
"orgSystemEvents",
|
|
136
|
+
] as const satisfies readonly OrgScopedType[];
|
|
137
|
+
|
|
138
|
+
export const USER_SCOPED_TYPES = [
|
|
139
|
+
"dismissedBanners",
|
|
140
|
+
"userOrgs",
|
|
141
|
+
"sessions",
|
|
142
|
+
"userMd",
|
|
143
|
+
"viewer",
|
|
144
|
+
] as const satisfies readonly UserScopedType[];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* System-scoped types (ADR-CONTRACTS-052) — tenant-less super-admin resources.
|
|
148
|
+
* Their query key is [type, 'system']; they carry no orgId or userId.
|
|
149
|
+
* Exported only for `resource-keys.ts`, the parser that reads them; like the other
|
|
150
|
+
* scope arrays they are not part of the package barrel.
|
|
151
|
+
*/
|
|
152
|
+
export const SYSTEM_SCOPED_TYPES = [
|
|
153
|
+
"internalAdminAiProviders",
|
|
154
|
+
"internalAdminPrompts",
|
|
155
|
+
"internalAdminAiRuntimeDefaults",
|
|
156
|
+
"factoryFloor",
|
|
157
|
+
"factorySnapshot",
|
|
158
|
+
"factoryKpis",
|
|
159
|
+
] as const satisfies readonly SystemScopedType[];
|
|
160
|
+
|
|
161
|
+
/** Every literal `fromQueryKey` can route, across all five tables. */
|
|
162
|
+
type RoutedType =
|
|
163
|
+
| keyof typeof IDENTITY_FIELDS
|
|
164
|
+
| keyof typeof COMPOSITE_IDENTITY_FIELDS
|
|
165
|
+
| (typeof ORG_SCOPED_TYPES)[number]
|
|
166
|
+
| (typeof USER_SCOPED_TYPES)[number]
|
|
167
|
+
| (typeof SYSTEM_SCOPED_TYPES)[number];
|
|
168
|
+
|
|
169
|
+
type Assert<T extends true> = T;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* THE DRIFT DETECTOR. Register a union member and forget its routing table and
|
|
173
|
+
* this fails to compile — instead of shipping a key that parses nowhere,
|
|
174
|
+
* matches nothing, and disables the query that reads it.
|
|
175
|
+
*
|
|
176
|
+
* The false branch resolves to the UNROUTED LITERALS rather than to `false`, so
|
|
177
|
+
* the diagnostic names the culprit:
|
|
178
|
+
* `Type '"directGrants"' does not satisfy the constraint 'true'`.
|
|
179
|
+
*
|
|
180
|
+
* EXPORTED, though nothing consumes it. This package ships `src`, so every
|
|
181
|
+
* consumer typechecks this file under ITS OWN compiler options — and the
|
|
182
|
+
* backend sets `noUnusedLocals`, which rejects an unexported type alias that
|
|
183
|
+
* nothing references. Keeping it private broke `tsc` in a consumer while
|
|
184
|
+
* passing here, so the export is what makes the assertion portable, not a
|
|
185
|
+
* widening of the public vocabulary. Do not "tidy" it away.
|
|
186
|
+
*/
|
|
187
|
+
export type RoutingExhaustivenessWitness = Assert<
|
|
188
|
+
[Exclude<ResourceKey["type"], RoutedType>] extends [never]
|
|
189
|
+
? true
|
|
190
|
+
: Exclude<ResourceKey["type"], RoutedType>
|
|
191
|
+
>;
|