@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.
@@ -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 { HrConnectionStatusSchema, HrConnectInputSchema } from "./schemas";
6
+ export {
7
+ HrConnectionStatusSchema,
8
+ HrConnectInputSchema,
9
+ HrisPurgeImpactSchema,
10
+ } from "./schemas";
7
11
 
8
- export type { HrConnectionStatus, HrConnectInput } from "./schemas";
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 question the engine is asking a human, with the competing signals attached.
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
- recommended: "u2",
225
- alternatives: ["u1"],
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
@@ -251,6 +251,7 @@ export {
251
251
  ProposedOrgUnitSchema,
252
252
  PersonStructureOutcomeSchema,
253
253
  StructureReviewItemSchema,
254
+ STRUCTURE_REVIEW_ITEM_KINDS,
254
255
  StructureProposalSchema,
255
256
  StructurePersonFactSchema,
256
257
  StructureReportingFactSchema,
@@ -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 StructureReviewItemSchema = z.object({
382
- kind: z.enum([
383
- "placement_conflict",
384
- "unit_boundary",
385
- "unit_name",
386
- "unit_head",
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
- /** The question, phrased for a human reviewer. */
393
- question: z.string(),
394
- /** What the engine would do absent an answer. */
395
- recommended: z.string(),
396
- /** The other defensible answers. */
397
- alternatives: z.array(z.string()),
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
+ >;