@company-semantics/contracts 62.5.0 → 62.7.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 +5 -5
- package/src/__tests__/iso-datetime.test.ts +71 -0
- package/src/admin/direct-grants.ts +4 -3
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +2 -3
- package/src/api/primitives.ts +44 -0
- package/src/chat/README.md +1 -1
- package/src/chat/index.ts +0 -1
- package/src/chat/schemas.ts +20 -26
- package/src/execution/README.md +1 -0
- package/src/execution/schemas.ts +3 -2
- package/src/execution/sse.ts +2 -1
- package/src/identity/README.md +1 -0
- package/src/identity/identity-link.ts +3 -2
- package/src/impersonation/schemas.ts +7 -6
- package/src/index.ts +15 -0
- package/src/integrations/README.md +1 -1
- package/src/integrations/schemas.ts +2 -1
- package/src/meetings/schemas.ts +4 -3
- package/src/message-parts/README.md +5 -0
- package/src/message-parts/__tests__/workflow-progress.test.ts +95 -0
- package/src/message-parts/index.ts +7 -0
- package/src/message-parts/types.ts +7 -1
- package/src/message-parts/wire.ts +29 -0
- package/src/message-parts/workflow-progress.ts +66 -0
- package/src/org/occupancy.ts +3 -2
- package/src/org/reasoning-review.ts +2 -1
- package/src/org/reconciliation.ts +4 -3
- package/src/org/schemas.ts +4 -4
- package/src/org/sync-run.ts +3 -2
- package/src/permissions/permission-introspection.ts +2 -1
- package/src/permissions/share-api.ts +2 -1
- package/src/security/org-secrets.ts +4 -3
- package/src/user-events/schemas.ts +2 -2
- package/src/workflows/README.md +35 -0
- package/src/workflows/__tests__/README.md +30 -0
- package/src/workflows/__tests__/types.test.ts +52 -0
- package/src/workflows/index.ts +12 -0
- package/src/workflows/types.ts +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "62.
|
|
3
|
+
"version": "62.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -139,8 +139,8 @@
|
|
|
139
139
|
"node": "22.x"
|
|
140
140
|
},
|
|
141
141
|
"dependencies": {
|
|
142
|
-
"@slack/types": "^3.
|
|
143
|
-
"zod": "^4.
|
|
142
|
+
"@slack/types": "^3.1.0",
|
|
143
|
+
"zod": "^4.5.2"
|
|
144
144
|
},
|
|
145
145
|
"devDependencies": {
|
|
146
146
|
"@eslint/js": "^10.0.1",
|
|
@@ -149,13 +149,13 @@
|
|
|
149
149
|
"culori": "^4.0.2",
|
|
150
150
|
"eslint": "^10.9.0",
|
|
151
151
|
"husky": "^9.1.7",
|
|
152
|
-
"lint-staged": "^17.
|
|
152
|
+
"lint-staged": "^17.4.1",
|
|
153
153
|
"markdownlint-cli2": "^0.23.2",
|
|
154
154
|
"openapi-typescript": "^7.13.0",
|
|
155
155
|
"prettier": "^3.9.6",
|
|
156
156
|
"tsx": "^4.23.12",
|
|
157
157
|
"typescript": "^5.8.3",
|
|
158
|
-
"typescript-eslint": "^8.
|
|
158
|
+
"typescript-eslint": "^8.68.0",
|
|
159
159
|
"vite": "^8.2.2",
|
|
160
160
|
"vitest": "^4.1.11",
|
|
161
161
|
"yaml": "^2.9.0"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ISO_DATETIME_OFFSET_PATTERN,
|
|
6
|
+
ISO_DATETIME_PATTERN,
|
|
7
|
+
IsoDateTime,
|
|
8
|
+
IsoDateTimeWithOffset,
|
|
9
|
+
} from "../api/primitives";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* These are drift alarms, not unit tests.
|
|
13
|
+
*
|
|
14
|
+
* `IsoDateTime` delegates to zod's built-in RFC 3339 regex, and zod is advanced
|
|
15
|
+
* automatically by Dependabot inside a grouped `minor`/`patch` bump. That is
|
|
16
|
+
* exactly how the seconds-optional -> seconds-mandatory tightening reached
|
|
17
|
+
* production undocumented (backend #520). If zod moves this regex again, the
|
|
18
|
+
* documented format of every `format: date-time` field on the public API moves
|
|
19
|
+
* with it — so it fails here first and gets ratified deliberately.
|
|
20
|
+
*
|
|
21
|
+
* If one of these fails: do not update the constant to make it green. Work out
|
|
22
|
+
* what zod changed, decide whether we want it, and record the decision.
|
|
23
|
+
*/
|
|
24
|
+
describe("the RFC 3339 wire contract is pinned, not inherited", () => {
|
|
25
|
+
it("emits the pinned pattern for the Z-only form", () => {
|
|
26
|
+
expect(z.toJSONSchema(IsoDateTime, { io: "input" }).pattern).toBe(
|
|
27
|
+
ISO_DATETIME_PATTERN,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("emits the pinned pattern for the offset-allowing form", () => {
|
|
32
|
+
expect(z.toJSONSchema(IsoDateTimeWithOffset, { io: "input" }).pattern).toBe(
|
|
33
|
+
ISO_DATETIME_OFFSET_PATTERN,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("still emits format: date-time, which the pattern alone would not carry", () => {
|
|
38
|
+
expect(z.toJSONSchema(IsoDateTime, { io: "input" }).format).toBe(
|
|
39
|
+
"date-time",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("seconds are mandatory (ADR-CONTRACTS-156)", () => {
|
|
45
|
+
it.each([
|
|
46
|
+
"2026-09-01T12:00:00Z",
|
|
47
|
+
"2026-09-01T12:00:00.000Z",
|
|
48
|
+
"2024-02-29T23:59:59Z",
|
|
49
|
+
])("accepts %s", (value) => {
|
|
50
|
+
expect(IsoDateTime.safeParse(value).success).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each(["2026-09-01T12:00Z", "2026-09-01T12Z", "2026-09-01"])(
|
|
54
|
+
"rejects %s",
|
|
55
|
+
(value) => {
|
|
56
|
+
expect(IsoDateTime.safeParse(value).success).toBe(false);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
it("rejects a numeric offset on the Z-only form", () => {
|
|
61
|
+
expect(IsoDateTime.safeParse("2026-09-01T12:00:00+02:00").success).toBe(
|
|
62
|
+
false,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("accepts a numeric offset on the offset-allowing form", () => {
|
|
67
|
+
expect(
|
|
68
|
+
IsoDateTimeWithOffset.safeParse("2026-09-01T12:00:00+02:00").success,
|
|
69
|
+
).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* direct-grant-audit-trail and the `direct-grant-audit` CI guard).
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { IsoDateTime } from "../api/primitives";
|
|
10
11
|
|
|
11
12
|
export const DirectGrantCreate = z.object({
|
|
12
13
|
subject_id: z.string().uuid(),
|
|
13
14
|
scope_pattern: z.string(),
|
|
14
15
|
resource_filter: z.record(z.string(), z.unknown()).optional(),
|
|
15
|
-
expires_at:
|
|
16
|
+
expires_at: IsoDateTime.optional(),
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
export type DirectGrantCreate = z.infer<typeof DirectGrantCreate>;
|
|
@@ -24,8 +25,8 @@ export const DirectGrant = z.object({
|
|
|
24
25
|
scope_pattern: z.string(),
|
|
25
26
|
source: z.literal("direct"),
|
|
26
27
|
granted_by: z.string().uuid(),
|
|
27
|
-
granted_at:
|
|
28
|
-
expires_at:
|
|
28
|
+
granted_at: IsoDateTime,
|
|
29
|
+
expires_at: IsoDateTime.nullable(),
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
export type DirectGrant = z.infer<typeof DirectGrant>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '52d89a463432' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '52d89a4634328ae80200c221d6aa25d8c34c9d09208a2c460d82a523ec79144d' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -6795,9 +6795,8 @@ export interface components {
|
|
|
6795
6795
|
p95ContextTokens: number;
|
|
6796
6796
|
medianReadsAfterFirstEdit: number;
|
|
6797
6797
|
}[];
|
|
6798
|
-
bySource:
|
|
6798
|
+
bySource: {
|
|
6799
6799
|
source: string;
|
|
6800
|
-
} & {
|
|
6801
6800
|
p95Reads?: number;
|
|
6802
6801
|
p50Reads?: number;
|
|
6803
6802
|
p95ContextTokens?: number;
|
|
@@ -6809,7 +6808,7 @@ export interface components {
|
|
|
6809
6808
|
navMapUsageBySource?: {
|
|
6810
6809
|
[key: string]: number;
|
|
6811
6810
|
};
|
|
6812
|
-
}
|
|
6811
|
+
}[];
|
|
6813
6812
|
trend: {
|
|
6814
6813
|
recent: {
|
|
6815
6814
|
p95Reads?: number;
|
package/src/api/primitives.ts
CHANGED
|
@@ -25,3 +25,47 @@ export type CursorPage<T> = {
|
|
|
25
25
|
hasMore: boolean;
|
|
26
26
|
};
|
|
27
27
|
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// The RFC 3339 timestamp contract
|
|
31
|
+
// =============================================================================
|
|
32
|
+
//
|
|
33
|
+
// Every `format: date-time` field on the public API surface is built from one of
|
|
34
|
+
// the two schemas below, so the wire contract has exactly one definition site.
|
|
35
|
+
//
|
|
36
|
+
// The validators delegate to zod's built-in `.datetime()` rather than
|
|
37
|
+
// reimplementing RFC 3339 by hand: that keeps `format: date-time` in the emitted
|
|
38
|
+
// OpenAPI document and avoids a hand-rolled regex drifting from the runtime
|
|
39
|
+
// check. The regex zod produces is therefore an *input* to our published
|
|
40
|
+
// contract, and zod is a transitive dependency that Dependabot advances
|
|
41
|
+
// automatically — so the emitted pattern is pinned below and asserted in
|
|
42
|
+
// `src/__tests__/iso-datetime.test.ts`.
|
|
43
|
+
//
|
|
44
|
+
// That assertion is the point. A zod release that changes this regex changes the
|
|
45
|
+
// documented format of 52 public fields; it must fail a test and be ratified,
|
|
46
|
+
// not land silently inside a grouped `minor`/`patch` bump. See ADR-CONTRACTS-156.
|
|
47
|
+
|
|
48
|
+
/** Emitted `pattern` for {@link IsoDateTime}. Pinned — see the note above. */
|
|
49
|
+
export const ISO_DATETIME_PATTERN =
|
|
50
|
+
"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$";
|
|
51
|
+
|
|
52
|
+
/** Emitted `pattern` for {@link IsoDateTimeWithOffset}. Pinned — see the note above. */
|
|
53
|
+
export const ISO_DATETIME_OFFSET_PATTERN =
|
|
54
|
+
"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A UTC RFC 3339 timestamp — `2026-09-01T12:00:00.000Z`.
|
|
58
|
+
*
|
|
59
|
+
* Seconds are mandatory. `2026-09-01T12:00Z` is rejected: it is not valid
|
|
60
|
+
* RFC 3339, and the backend has rejected it since zod 4.5.x.
|
|
61
|
+
*/
|
|
62
|
+
export const IsoDateTime = z.string().datetime();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* An RFC 3339 timestamp that may carry a numeric UTC offset instead of `Z` —
|
|
66
|
+
* `2026-09-01T12:00:00.000+02:00`.
|
|
67
|
+
*
|
|
68
|
+
* Use only where a client legitimately reports local wall-clock time with its
|
|
69
|
+
* offset. Prefer {@link IsoDateTime} for anything the server mints.
|
|
70
|
+
*/
|
|
71
|
+
export const IsoDateTimeWithOffset = z.string().datetime({ offset: true });
|
package/src/chat/README.md
CHANGED
|
@@ -89,7 +89,6 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
|
|
|
89
89
|
- `InvalidateChatListEventSchema`
|
|
90
90
|
- `InvalidationReason` _(type)_ — Reason for cache invalidation.
|
|
91
91
|
- `InvalidationReasonSchema`
|
|
92
|
-
- `IsoDateString` — ISO 8601 datetime string — the runtime safety net for date serialization.
|
|
93
92
|
- `ListSharesResponse` _(type)_ — Response for GET /api/chats/:chatId/shares
|
|
94
93
|
- `ListSharesResponseSchema` — Response for GET /api/chats/:chatId/shares
|
|
95
94
|
- `ProactiveChatResolution` _(type)_ — Resolving an occurrence id to the CURRENT reader's chat — one of the four bounded states `ready` (with…
|
|
@@ -117,6 +116,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
|
|
|
117
116
|
|
|
118
117
|
**Internal domains:**
|
|
119
118
|
|
|
119
|
+
- `api`
|
|
120
120
|
- `proactive`
|
|
121
121
|
|
|
122
122
|
**External packages:**
|
package/src/chat/index.ts
CHANGED
package/src/chat/schemas.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Reusable Primitives
|
|
5
|
-
// =============================================================================
|
|
6
|
-
|
|
7
|
-
/** ISO 8601 datetime string — the runtime safety net for date serialization. */
|
|
8
|
-
export const IsoDateString = z.string().datetime();
|
|
2
|
+
import { IsoDateTime } from "../api/primitives";
|
|
9
3
|
|
|
10
4
|
// =============================================================================
|
|
11
5
|
// Enums
|
|
@@ -78,15 +72,15 @@ export const ChatSummarySchema = z.object({
|
|
|
78
72
|
id: z.string(),
|
|
79
73
|
title: z.string().nullable(),
|
|
80
74
|
messageCount: z.number().int(),
|
|
81
|
-
createdAt:
|
|
82
|
-
updatedAt:
|
|
75
|
+
createdAt: IsoDateTime,
|
|
76
|
+
updatedAt: IsoDateTime,
|
|
83
77
|
isShared: z.boolean(),
|
|
84
78
|
});
|
|
85
79
|
|
|
86
80
|
export const ChatSummaryExtendedSchema = ChatSummarySchema.extend({
|
|
87
|
-
pinnedAt:
|
|
81
|
+
pinnedAt: IsoDateTime.nullable(),
|
|
88
82
|
titleSource: TitleSourceSchema.nullable(),
|
|
89
|
-
titleGeneratedAt:
|
|
83
|
+
titleGeneratedAt: IsoDateTime.nullable(),
|
|
90
84
|
...proactiveChatFields,
|
|
91
85
|
});
|
|
92
86
|
|
|
@@ -129,7 +123,7 @@ export const ChatShareInfoSchema = z.object({
|
|
|
129
123
|
shareUrl: z.string().optional(),
|
|
130
124
|
messageCountAtShare: z.number().int(),
|
|
131
125
|
titleAtShare: z.string(),
|
|
132
|
-
createdAt:
|
|
126
|
+
createdAt: IsoDateTime,
|
|
133
127
|
createdByName: z.string(),
|
|
134
128
|
isRevoked: z.boolean(),
|
|
135
129
|
});
|
|
@@ -137,7 +131,7 @@ export const ChatShareInfoSchema = z.object({
|
|
|
137
131
|
export const SharedChatViewSchema = z.object({
|
|
138
132
|
title: z.string(),
|
|
139
133
|
messages: z.array(SharedChatMessageSchema),
|
|
140
|
-
sharedAt:
|
|
134
|
+
sharedAt: IsoDateTime,
|
|
141
135
|
sharedByName: z.string(),
|
|
142
136
|
visibility: ChatVisibilitySchema,
|
|
143
137
|
});
|
|
@@ -148,7 +142,7 @@ export const SharedChatViewSchema = z.object({
|
|
|
148
142
|
|
|
149
143
|
export const BaseEventSchema = z.object({
|
|
150
144
|
v: z.literal(1),
|
|
151
|
-
timestamp:
|
|
145
|
+
timestamp: IsoDateTime,
|
|
152
146
|
eventId: z.string().optional(),
|
|
153
147
|
});
|
|
154
148
|
|
|
@@ -167,10 +161,10 @@ export const ChatCreatedEventSchema = BaseEventSchema.extend({
|
|
|
167
161
|
interactionId: z.string().optional(),
|
|
168
162
|
title: z.string(),
|
|
169
163
|
titleSource: TitleSourceSchema,
|
|
170
|
-
pinnedAt:
|
|
164
|
+
pinnedAt: IsoDateTime.nullable(),
|
|
171
165
|
messageCount: z.number().int(),
|
|
172
|
-
createdAt:
|
|
173
|
-
updatedAt:
|
|
166
|
+
createdAt: IsoDateTime,
|
|
167
|
+
updatedAt: IsoDateTime,
|
|
174
168
|
isShared: z.boolean(),
|
|
175
169
|
...proactiveChatFields,
|
|
176
170
|
}),
|
|
@@ -182,11 +176,11 @@ export const ChatUpdatedEventSchema = BaseEventSchema.extend({
|
|
|
182
176
|
chatId: z.string(),
|
|
183
177
|
title: z.string(),
|
|
184
178
|
titleSource: TitleSourceSchema.nullable(),
|
|
185
|
-
titleGeneratedAt:
|
|
186
|
-
pinnedAt:
|
|
179
|
+
titleGeneratedAt: IsoDateTime.nullable(),
|
|
180
|
+
pinnedAt: IsoDateTime.nullable(),
|
|
187
181
|
messageCount: z.number().int(),
|
|
188
|
-
createdAt:
|
|
189
|
-
updatedAt:
|
|
182
|
+
createdAt: IsoDateTime,
|
|
183
|
+
updatedAt: IsoDateTime,
|
|
190
184
|
isShared: z.boolean(),
|
|
191
185
|
...proactiveChatFields,
|
|
192
186
|
}),
|
|
@@ -245,7 +239,7 @@ export const ChatMessageSchema = z.object({
|
|
|
245
239
|
content: z.string(),
|
|
246
240
|
parts: z.array(z.unknown()).optional(),
|
|
247
241
|
sequenceNumber: z.number().int(),
|
|
248
|
-
createdAt:
|
|
242
|
+
createdAt: IsoDateTime,
|
|
249
243
|
});
|
|
250
244
|
|
|
251
245
|
// =============================================================================
|
|
@@ -325,11 +319,11 @@ export const ChatDetailSchema = z.object({
|
|
|
325
319
|
id: z.string(),
|
|
326
320
|
title: z.string(),
|
|
327
321
|
interactionId: z.string(),
|
|
328
|
-
pinnedAt:
|
|
322
|
+
pinnedAt: IsoDateTime.nullable().optional(),
|
|
329
323
|
titleSource: TitleSourceSchema.nullable().optional(),
|
|
330
|
-
titleGeneratedAt:
|
|
331
|
-
createdAt:
|
|
332
|
-
updatedAt:
|
|
324
|
+
titleGeneratedAt: IsoDateTime.nullable().optional(),
|
|
325
|
+
createdAt: IsoDateTime,
|
|
326
|
+
updatedAt: IsoDateTime,
|
|
333
327
|
// Opening the detail is what clears `unread`; the detail carries the flag so
|
|
334
328
|
// the reader can tell a first open from a return visit. `proactiveKind` is
|
|
335
329
|
// a list concern (the sidebar row) and does not ride the detail.
|
package/src/execution/README.md
CHANGED
package/src/execution/schemas.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { z } from "zod";
|
|
11
|
+
import { IsoDateTimeWithOffset } from "../api/primitives";
|
|
11
12
|
|
|
12
13
|
import type { ExecutionResultData } from "../message-parts/execution";
|
|
13
14
|
import type { ExecutionKind } from "./kinds";
|
|
@@ -22,8 +23,8 @@ export const ExecutionSummarySchema = z.object({
|
|
|
22
23
|
executionId: z.string().uuid(),
|
|
23
24
|
kind: z.string(),
|
|
24
25
|
status: z.enum(["pending", "succeeded", "failed"]),
|
|
25
|
-
decidedAt:
|
|
26
|
-
completedAt:
|
|
26
|
+
decidedAt: IsoDateTimeWithOffset,
|
|
27
|
+
completedAt: IsoDateTimeWithOffset.optional(),
|
|
27
28
|
target: z.object({ type: z.string() }),
|
|
28
29
|
});
|
|
29
30
|
|
package/src/execution/sse.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { z } from "zod";
|
|
18
|
+
import { IsoDateTimeWithOffset } from "../api/primitives";
|
|
18
19
|
import { ExecutionEventTypeSchema } from "./events";
|
|
19
20
|
import { ExecutionStateSchema } from "./status";
|
|
20
21
|
|
|
@@ -27,7 +28,7 @@ export const ExecutionSseEventSchema = z.object({
|
|
|
27
28
|
/** Coarse actor class that produced the event (e.g. `user`, `system`). */
|
|
28
29
|
actorType: z.string(),
|
|
29
30
|
/** DB insert time of the event, ISO-8601 with offset. */
|
|
30
|
-
createdAt:
|
|
31
|
+
createdAt: IsoDateTimeWithOffset,
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
export type ExecutionSseEvent = z.infer<typeof ExecutionSseEventSchema>;
|
package/src/identity/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* @see ADR-CONT-032 for identity vocabulary design rationale
|
|
18
18
|
*/
|
|
19
19
|
import { z } from "zod";
|
|
20
|
+
import { IsoDateTime } from "../api/primitives";
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// IdentityLink — external system id → Person
|
|
@@ -31,8 +32,8 @@ export const IdentityLinkSchema = z.object({
|
|
|
31
32
|
externalId: z.string().min(1),
|
|
32
33
|
// The Person this external identity resolves to (Person.id).
|
|
33
34
|
personId: z.string().uuid(),
|
|
34
|
-
createdAt:
|
|
35
|
-
updatedAt:
|
|
35
|
+
createdAt: IsoDateTime,
|
|
36
|
+
updatedAt: IsoDateTime,
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
export type IdentityLink = z.infer<typeof IdentityLinkSchema>;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
import { IsoDateTimeWithOffset } from "../api/primitives";
|
|
12
13
|
|
|
13
14
|
export const ImpersonationSessionSchema = z.object({
|
|
14
15
|
impersonationSessionId: z.string(),
|
|
@@ -16,9 +17,9 @@ export const ImpersonationSessionSchema = z.object({
|
|
|
16
17
|
targetUserId: z.string(),
|
|
17
18
|
reason: z.string(),
|
|
18
19
|
reasonHash: z.string(),
|
|
19
|
-
startedAt:
|
|
20
|
-
expiresAt:
|
|
21
|
-
endedAt:
|
|
20
|
+
startedAt: IsoDateTimeWithOffset,
|
|
21
|
+
expiresAt: IsoDateTimeWithOffset,
|
|
22
|
+
endedAt: IsoDateTimeWithOffset.nullable(),
|
|
22
23
|
ipAddress: z.string(),
|
|
23
24
|
userAgent: z.string(),
|
|
24
25
|
});
|
|
@@ -48,9 +49,9 @@ export const ImpersonationSessionWireDtoSchema = z
|
|
|
48
49
|
.object({
|
|
49
50
|
id: z.string(),
|
|
50
51
|
targetUserId: z.string(),
|
|
51
|
-
startedAt:
|
|
52
|
-
expiresAt:
|
|
53
|
-
endedAt:
|
|
52
|
+
startedAt: IsoDateTimeWithOffset,
|
|
53
|
+
expiresAt: IsoDateTimeWithOffset,
|
|
54
|
+
endedAt: IsoDateTimeWithOffset.nullable(),
|
|
54
55
|
})
|
|
55
56
|
.strict();
|
|
56
57
|
|
package/src/index.ts
CHANGED
|
@@ -367,6 +367,17 @@ export type {
|
|
|
367
367
|
ProactiveChatMessage,
|
|
368
368
|
} from "./proactive/index";
|
|
369
369
|
|
|
370
|
+
// Analysis workflows — the registered types a durable long-running analysis
|
|
371
|
+
// can be (PRD-00976). The workflow primitive itself is domain-agnostic; its
|
|
372
|
+
// TYPE is the one place a domain names itself, and that name is a public wire
|
|
373
|
+
// discriminator carried on the workflow row, in the data-workflow-progress
|
|
374
|
+
// chat part and by the generic projection read. Namespaced `<domain>/<flow>`
|
|
375
|
+
// so domains cannot collide, and a CLOSED const array so the discriminator is
|
|
376
|
+
// validated and handled exhaustively. See src/workflows/README.md.
|
|
377
|
+
export { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "./workflows/index";
|
|
378
|
+
|
|
379
|
+
export type { WorkflowType } from "./workflows/index";
|
|
380
|
+
|
|
370
381
|
// Comment wire contract — where a thread hangs (the anchor union), what a
|
|
371
382
|
// thread and its comments look like on the wire, and who a comment may name.
|
|
372
383
|
// The anchor is the single most load-bearing shape in this package: the backend
|
|
@@ -1032,6 +1043,10 @@ export type {
|
|
|
1032
1043
|
SuggestedRepliesData,
|
|
1033
1044
|
SuggestedRepliesPart,
|
|
1034
1045
|
SuggestedRepliesDataPart,
|
|
1046
|
+
// Workflow progress surface types (non-governed anchor, data-workflow-progress, PRD-00976)
|
|
1047
|
+
WorkflowProgressData,
|
|
1048
|
+
WorkflowProgressPart,
|
|
1049
|
+
WorkflowProgressDataPart,
|
|
1035
1050
|
// Execution result types (Phase 5)
|
|
1036
1051
|
ExecutionArtifactStatus,
|
|
1037
1052
|
ExecutionResultSummary,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* stringified JSON blobs on the schema.
|
|
17
17
|
*/
|
|
18
18
|
import { z } from "zod";
|
|
19
|
+
import { IsoDateTime } from "../api/primitives";
|
|
19
20
|
|
|
20
21
|
// =============================================================================
|
|
21
22
|
// HR connection status
|
|
@@ -87,7 +88,7 @@ export const HrConnectionStatusSchema = z
|
|
|
87
88
|
description:
|
|
88
89
|
"Departments mirrored from the HR provider; null when never synced.",
|
|
89
90
|
}),
|
|
90
|
-
lastSyncAt:
|
|
91
|
+
lastSyncAt: IsoDateTime.nullable().meta({
|
|
91
92
|
description: "ISO timestamp of the last successful sync; null if never.",
|
|
92
93
|
}),
|
|
93
94
|
syncHealth: z.enum(["healthy", "degraded", "error"]).nullable().meta({
|
package/src/meetings/schemas.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* stringified JSON blobs on the schema.
|
|
22
22
|
*/
|
|
23
23
|
import { z } from "zod";
|
|
24
|
+
import { IsoDateTime } from "../api/primitives";
|
|
24
25
|
|
|
25
26
|
// =============================================================================
|
|
26
27
|
// Recording identity
|
|
@@ -237,7 +238,7 @@ export const TranscriptionSessionGrantSchema = z
|
|
|
237
238
|
sessionId: RecordingIdSchema,
|
|
238
239
|
connectUrl: z.string().url(),
|
|
239
240
|
authHeaders: z.record(z.string(), z.string()),
|
|
240
|
-
expiresAt:
|
|
241
|
+
expiresAt: IsoDateTime,
|
|
241
242
|
audioConfig: z.object({
|
|
242
243
|
sampleRateHz: z.literal(16000),
|
|
243
244
|
channels: z.literal(2),
|
|
@@ -279,8 +280,8 @@ export const MeetingMetadataProjectionSchema = z
|
|
|
279
280
|
visibility: MeetingVisibilitySchema,
|
|
280
281
|
status: RecordingStatusSchema,
|
|
281
282
|
quality: RecordingQualitySchema,
|
|
282
|
-
startedAt:
|
|
283
|
-
endedAt:
|
|
283
|
+
startedAt: IsoDateTime,
|
|
284
|
+
endedAt: IsoDateTime.nullable(),
|
|
284
285
|
durationMs: z.number().int().nonnegative(),
|
|
285
286
|
participantUserIds: z.array(z.string().uuid()),
|
|
286
287
|
transcriptAvailable: z.boolean(),
|
|
@@ -15,6 +15,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
|
|
|
15
15
|
- Execution results carry `state` resolved from ExecutionState (single authority, no redundant status)
|
|
16
16
|
- Undo creates append-only audit rows — original execution is never mutated
|
|
17
17
|
- `SuggestedRepliesPart` is a surface but NOT a governed one: a chip fires an ordinary user turn (no `executionId`, no `submitEndpoint`), is stateless (clicking never consumes it) and advisory (the composer stays enabled) — so it is outside the at-most-one-governed-surface-per-turn rule
|
|
18
|
+
- `WorkflowProgressPart` is the second non-governed surface: a two-field ANCHOR (`workflowId`, `workflowType`) to a durable background workflow. It presents no decision, carries no result snapshot (minted at t=0; parts are never rewritten) and no `startedAt` (operational state stays off immutable content), and names no domain vocabulary — live state is hydrated from the generic workflow projection read
|
|
18
19
|
|
|
19
20
|
<!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
|
|
20
21
|
|
|
@@ -82,6 +83,9 @@ Canonical vocabulary for structured assistant message output. Defines the type s
|
|
|
82
83
|
- `UndoResultData` _(type)_ — Undo result data payload.
|
|
83
84
|
- `UndoResultDataPart` _(type)_ — Undo result data part (wire format).
|
|
84
85
|
- `WireSurfaceBuilder` — Factory for creating wire-format surface parts.
|
|
86
|
+
- `WorkflowProgressData` _(type)_ — Workflow progress surface data payload — the anchor's handle and its discriminator.
|
|
87
|
+
- `WorkflowProgressDataPart` _(type)_ — Workflow progress data part (wire format, as persisted in `chat_messages.parts`).
|
|
88
|
+
- `WorkflowProgressPart` _(type)_ — Workflow progress message part (semantic type, as rendered).
|
|
85
89
|
- `addPart` — Add any message part to the builder.
|
|
86
90
|
- `addSurface` — Add a surface part to the builder.
|
|
87
91
|
- `addText` — Add a text part to the builder.
|
|
@@ -102,6 +106,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
|
|
|
102
106
|
|
|
103
107
|
- `execution`
|
|
104
108
|
- `mcp`
|
|
109
|
+
- `workflows`
|
|
105
110
|
|
|
106
111
|
**External packages:**
|
|
107
112
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { WireSurfaceBuilder } from "../wire";
|
|
3
|
+
import { isSurfacePart, isTextPart } from "../types";
|
|
4
|
+
import type { AssistantMessagePart, SurfacePart } from "../types";
|
|
5
|
+
import { createPartBuilder, addSurface, addText } from "../builder";
|
|
6
|
+
import type {
|
|
7
|
+
WorkflowProgressData,
|
|
8
|
+
WorkflowProgressPart,
|
|
9
|
+
} from "../workflow-progress";
|
|
10
|
+
|
|
11
|
+
describe("WireSurfaceBuilder.workflowProgress", () => {
|
|
12
|
+
const data: WorkflowProgressData = {
|
|
13
|
+
workflowId: "3f1c2d4e-0000-0000-0000-000000000000",
|
|
14
|
+
workflowType: "org-structure/hris-connect",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
it("returns data part with type data-workflow-progress", () => {
|
|
18
|
+
const result = WireSurfaceBuilder.workflowProgress(data);
|
|
19
|
+
expect(result.type).toBe("data-workflow-progress");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("data matches input WorkflowProgressData exactly", () => {
|
|
23
|
+
const result = WireSurfaceBuilder.workflowProgress(data);
|
|
24
|
+
expect(result.data).toEqual(data);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// The part is an ANCHOR: a handle plus a discriminator and nothing else.
|
|
28
|
+
// Pinning the key set catches the two documented regressions — a `result`
|
|
29
|
+
// snapshot (minted at t=0, it could only ever be empty or stale) or a
|
|
30
|
+
// `startedAt` (operational state duplicated into immutable content) creeping
|
|
31
|
+
// onto a durably persisted, share-visible payload.
|
|
32
|
+
it("workflow-progress carries exactly the anchor keys", () => {
|
|
33
|
+
const result = WireSurfaceBuilder.workflowProgress(data);
|
|
34
|
+
expect(Object.keys(result.data).sort()).toEqual([
|
|
35
|
+
"workflowId",
|
|
36
|
+
"workflowType",
|
|
37
|
+
]);
|
|
38
|
+
expect(result.data).not.toHaveProperty("result");
|
|
39
|
+
expect(result.data).not.toHaveProperty("startedAt");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Non-governed: nothing on the payload would make it count against the
|
|
43
|
+
// at-most-one-governed-surface-per-turn budget.
|
|
44
|
+
it("carries no governed-surface fields (no executionId, no submitEndpoint)", () => {
|
|
45
|
+
const result = WireSurfaceBuilder.workflowProgress(data);
|
|
46
|
+
expect(result.data).not.toHaveProperty("executionId");
|
|
47
|
+
expect(result.data).not.toHaveProperty("submitEndpoint");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Stateless: building twice from the same facts yields identical output, so
|
|
51
|
+
// a historical message re-rendered later cannot differ from the original.
|
|
52
|
+
it("is deterministic across repeated builds", () => {
|
|
53
|
+
expect(WireSurfaceBuilder.workflowProgress(data)).toEqual(
|
|
54
|
+
WireSurfaceBuilder.workflowProgress(data),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// The semantic part is what the app renders after normalizing the wire part.
|
|
60
|
+
// Typing the fixture as WorkflowProgressPart and then assigning it to
|
|
61
|
+
// SurfacePart is the compile-time proof that the union gained the member.
|
|
62
|
+
const anchor: WorkflowProgressPart = {
|
|
63
|
+
type: "workflow-progress",
|
|
64
|
+
data: {
|
|
65
|
+
workflowId: "3f1c2d4e-0000-0000-0000-000000000000",
|
|
66
|
+
workflowType: "org-structure/reanalyse",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe("WorkflowProgressPart in the SurfacePart union", () => {
|
|
71
|
+
it("is assignable to SurfacePart and AssistantMessagePart", () => {
|
|
72
|
+
const asSurface: SurfacePart = anchor;
|
|
73
|
+
const asPart: AssistantMessagePart = anchor;
|
|
74
|
+
expect(asSurface.type).toBe("workflow-progress");
|
|
75
|
+
expect(asPart.type).toBe("workflow-progress");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("is classified as a surface part, not a text part", () => {
|
|
79
|
+
expect(isSurfacePart(anchor)).toBe(true);
|
|
80
|
+
expect(isTextPart(anchor)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// The builder's narrative-before-surface state machine applies to the anchor
|
|
84
|
+
// like any other surface: it is emitted after the prose, never streamed.
|
|
85
|
+
it("is accepted by addSurface after narrative text", () => {
|
|
86
|
+
let state = createPartBuilder();
|
|
87
|
+
state = addText(state, "Analysing your org structure now.").state;
|
|
88
|
+
const result = addSurface(state, anchor);
|
|
89
|
+
expect(result.accepted).toBe(true);
|
|
90
|
+
expect(result.state.parts).toEqual([
|
|
91
|
+
{ type: "text", text: "Analysing your org structure now." },
|
|
92
|
+
anchor,
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -71,6 +71,13 @@ export type {
|
|
|
71
71
|
SuggestedRepliesDataPart,
|
|
72
72
|
} from "./suggested-replies";
|
|
73
73
|
|
|
74
|
+
// Workflow progress surface types (non-governed anchor to a durable workflow)
|
|
75
|
+
export type {
|
|
76
|
+
WorkflowProgressData,
|
|
77
|
+
WorkflowProgressPart,
|
|
78
|
+
WorkflowProgressDataPart,
|
|
79
|
+
} from "./workflow-progress";
|
|
80
|
+
|
|
74
81
|
// Execution result types
|
|
75
82
|
export type {
|
|
76
83
|
ExecutionArtifactStatus,
|
|
@@ -19,6 +19,7 @@ import type { ConfirmationPart } from "./confirmation";
|
|
|
19
19
|
import type { InteractiveTaskPart } from "./interactive";
|
|
20
20
|
import type { StructureReviewPart } from "./structure-review";
|
|
21
21
|
import type { SuggestedRepliesPart } from "./suggested-replies";
|
|
22
|
+
import type { WorkflowProgressPart } from "./workflow-progress";
|
|
22
23
|
|
|
23
24
|
// =============================================================================
|
|
24
25
|
// Narrative Parts (Streamable)
|
|
@@ -118,6 +119,10 @@ export interface TablePart {
|
|
|
118
119
|
* SuggestedRepliesPart is a surface part but NOT a governed one: a chip fires
|
|
119
120
|
* an ordinary user turn, so it is outside the preview/confirmation/interactive
|
|
120
121
|
* triad and its at-most-one-governed-surface-per-turn rule.
|
|
122
|
+
*
|
|
123
|
+
* WorkflowProgressPart is the second non-governed member: it anchors a durable
|
|
124
|
+
* background workflow to the occurrence and presents no decision, so it must
|
|
125
|
+
* not consume the governed-surface budget either.
|
|
121
126
|
*/
|
|
122
127
|
export type SurfacePart =
|
|
123
128
|
| ToolListPart
|
|
@@ -128,7 +133,8 @@ export type SurfacePart =
|
|
|
128
133
|
| PreviewPart
|
|
129
134
|
| InteractiveTaskPart
|
|
130
135
|
| StructureReviewPart
|
|
131
|
-
| SuggestedRepliesPart
|
|
136
|
+
| SuggestedRepliesPart
|
|
137
|
+
| WorkflowProgressPart;
|
|
132
138
|
|
|
133
139
|
/**
|
|
134
140
|
* All assistant message part types.
|
|
@@ -25,6 +25,10 @@ import type {
|
|
|
25
25
|
SuggestedRepliesData,
|
|
26
26
|
SuggestedRepliesDataPart,
|
|
27
27
|
} from "./suggested-replies";
|
|
28
|
+
import type {
|
|
29
|
+
WorkflowProgressData,
|
|
30
|
+
WorkflowProgressDataPart,
|
|
31
|
+
} from "./workflow-progress";
|
|
28
32
|
import type {
|
|
29
33
|
ExecutionResultData,
|
|
30
34
|
ExecutionResultDataPart,
|
|
@@ -164,6 +168,31 @@ export const WireSurfaceBuilder = {
|
|
|
164
168
|
};
|
|
165
169
|
},
|
|
166
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Build a workflow-progress data part for streaming.
|
|
173
|
+
* The anchor for a durable background workflow attached to this assistant
|
|
174
|
+
* occurrence: a handle plus the namespaced workflow type, hydrated
|
|
175
|
+
* client-side from the generic workflow projection read.
|
|
176
|
+
*
|
|
177
|
+
* INVARIANTS:
|
|
178
|
+
* - Not a governed surface: presents no decision, no executionId, no
|
|
179
|
+
* submitEndpoint — so it does not count against the
|
|
180
|
+
* at-most-one-governed-surface-per-turn rule
|
|
181
|
+
* - Anchor only: no result snapshot (minted at t=0, parts are never
|
|
182
|
+
* rewritten) and no startedAt (operational state stays off immutable
|
|
183
|
+
* content)
|
|
184
|
+
* - Emission is orchestration-driven (a typed launch), never a model choice
|
|
185
|
+
*
|
|
186
|
+
* @param data - Workflow progress data (workflowId + workflowType)
|
|
187
|
+
* @returns Wire-format workflow progress part ready for stream
|
|
188
|
+
*/
|
|
189
|
+
workflowProgress(data: WorkflowProgressData): WorkflowProgressDataPart {
|
|
190
|
+
return {
|
|
191
|
+
type: "data-workflow-progress",
|
|
192
|
+
data,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
|
|
167
196
|
/**
|
|
168
197
|
* Build an execution result data part for streaming.
|
|
169
198
|
* Reports the outcome of an executed action.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Progress Surface Types
|
|
3
|
+
*
|
|
4
|
+
* A durable background workflow attached to an assistant occurrence. The part
|
|
5
|
+
* tells a renderer "there is long-running work behind this message; render its
|
|
6
|
+
* progress surface" — and deliberately says nothing about what that work is
|
|
7
|
+
* analysing. It is a SIBLING of `suggested-replies`: a surface part, but NOT a
|
|
8
|
+
* governed one.
|
|
9
|
+
*
|
|
10
|
+
* WORKFLOW PROGRESS INVARIANTS:
|
|
11
|
+
* - AN ANCHOR, NOT A PAYLOAD. Chat message parts are persisted verbatim and
|
|
12
|
+
* shipped verbatim to token-only share readers, so this carries a handle and
|
|
13
|
+
* a type and nothing else. Live and settled state come from one authorized
|
|
14
|
+
* read keyed on `workflowId` (the generic workflow projection), never from
|
|
15
|
+
* the part.
|
|
16
|
+
* - NO `result`. The part is minted at t=0, before the workflow has produced
|
|
17
|
+
* anything, and `chat_messages.parts` is written once and never rewritten.
|
|
18
|
+
* A result snapshot here would be permanently empty or permanently stale.
|
|
19
|
+
* - NO `startedAt`. It would duplicate operational state into immutable
|
|
20
|
+
* message content, and nothing renders it without hydrating the workflow
|
|
21
|
+
* anyway.
|
|
22
|
+
* - NON-GOVERNED. It presents no decision: no `executionId`, no
|
|
23
|
+
* `submitEndpoint`, no side effect of its own. So it must NOT count against
|
|
24
|
+
* the at-most-one-governed-surface-per-turn budget the chat bridge enforces
|
|
25
|
+
* over the preview/confirmation/interactive triad.
|
|
26
|
+
* - NO DOMAIN VOCABULARY. `workflowType` is the namespaced discriminator from
|
|
27
|
+
* `ANALYSIS_WORKFLOW_TYPES`; the part never names a department, proposal,
|
|
28
|
+
* provider or person. A domain adapter on the consuming side owns that.
|
|
29
|
+
* - Emission is orchestration-driven (a typed launch), never a model choice.
|
|
30
|
+
*
|
|
31
|
+
* Purely additive: an app that predates this part normalizes unknown part
|
|
32
|
+
* types to null, so it drops the anchor and still renders the prose.
|
|
33
|
+
*
|
|
34
|
+
* @see ../workflows/types.ts for the registered workflow types
|
|
35
|
+
* @see decisions/ADR-CONT-026.md for the message-parts design
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type { WorkflowType } from "../workflows/types";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Workflow progress surface data payload — the anchor's handle and its
|
|
42
|
+
* discriminator. Both fields required; nothing else is ever carried.
|
|
43
|
+
*/
|
|
44
|
+
export interface WorkflowProgressData {
|
|
45
|
+
/** The durable workflow this occurrence is attached to. */
|
|
46
|
+
workflowId: string;
|
|
47
|
+
/** Which registered flow it is — the namespaced wire discriminator. */
|
|
48
|
+
workflowType: WorkflowType;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Workflow progress message part (semantic type, as rendered).
|
|
53
|
+
*/
|
|
54
|
+
export interface WorkflowProgressPart {
|
|
55
|
+
type: "workflow-progress";
|
|
56
|
+
data: WorkflowProgressData;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Workflow progress data part (wire format, as persisted in
|
|
61
|
+
* `chat_messages.parts`). Uses AI SDK's data-{name} convention.
|
|
62
|
+
*/
|
|
63
|
+
export interface WorkflowProgressDataPart {
|
|
64
|
+
type: "data-workflow-progress";
|
|
65
|
+
data: WorkflowProgressData;
|
|
66
|
+
}
|
package/src/org/occupancy.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* account.
|
|
26
26
|
*/
|
|
27
27
|
import { z } from "zod";
|
|
28
|
+
import { IsoDateTime } from "../api/primitives";
|
|
28
29
|
|
|
29
30
|
import { FactProvenanceSchema } from "./structure-facts";
|
|
30
31
|
|
|
@@ -60,9 +61,9 @@ export const OccupancySchema = z.object({
|
|
|
60
61
|
/** Lifecycle of this hold; only `active` makes the position occupied. */
|
|
61
62
|
status: OccupancyStatusSchema,
|
|
62
63
|
/** ISO start of the hold; `null`/absent when unknown or open-started. */
|
|
63
|
-
startsAt:
|
|
64
|
+
startsAt: IsoDateTime.nullable().optional(),
|
|
64
65
|
/** ISO end of the hold; `null`/absent while the hold is still open. */
|
|
65
|
-
endsAt:
|
|
66
|
+
endsAt: IsoDateTime.nullable().optional(),
|
|
66
67
|
/** Provenance of this occupancy fact (truth hierarchy + supersede model). */
|
|
67
68
|
provenance: FactProvenanceSchema,
|
|
68
69
|
});
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* `inferenceType` and is not part of the stable contract surface.
|
|
21
21
|
*/
|
|
22
22
|
import { z } from "zod";
|
|
23
|
+
import { IsoDateTime } from "../api/primitives";
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// OrgStructureReviewInference — one pending ('review') structure inference
|
|
@@ -43,7 +44,7 @@ export const OrgStructureReviewInferenceSchema = z.object({
|
|
|
43
44
|
/** Human-meaningful WHY the engine proposed this change. */
|
|
44
45
|
reason: z.string(),
|
|
45
46
|
/** ISO timestamp the inference was created. */
|
|
46
|
-
createdAt:
|
|
47
|
+
createdAt: IsoDateTime,
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
export type OrgStructureReviewInference = z.infer<
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* remove an entry is the two sides agreeing.
|
|
40
40
|
*/
|
|
41
41
|
import { z } from "zod";
|
|
42
|
+
import { IsoDateTime } from "../api/primitives";
|
|
42
43
|
|
|
43
44
|
import { FactSourceTierSchema } from "./structure-facts";
|
|
44
45
|
|
|
@@ -92,7 +93,7 @@ export const OrgDivergenceSchema = z.object({
|
|
|
92
93
|
*/
|
|
93
94
|
reason: z.string().max(2000).nullable(),
|
|
94
95
|
/** ISO timestamp the workspace's current word on this field was recorded. */
|
|
95
|
-
recordedAt:
|
|
96
|
+
recordedAt: IsoDateTime,
|
|
96
97
|
/**
|
|
97
98
|
* ISO timestamp someone in this workspace ACKNOWLEDGED this entry, or `null`.
|
|
98
99
|
*
|
|
@@ -102,7 +103,7 @@ export const OrgDivergenceSchema = z.object({
|
|
|
102
103
|
* the entry — arrives unacknowledged, which is the right default for a
|
|
103
104
|
* statement nobody has read yet.
|
|
104
105
|
*/
|
|
105
|
-
acknowledgedAt:
|
|
106
|
+
acknowledgedAt: IsoDateTime.nullable(),
|
|
106
107
|
/** Who acknowledged it. `null` when nobody has, or when that user is gone. */
|
|
107
108
|
acknowledgedByUserId: z.string().uuid().nullable(),
|
|
108
109
|
});
|
|
@@ -146,7 +147,7 @@ export const ResolveOrgDivergenceResultSchema = z.object({
|
|
|
146
147
|
/** How it was resolved. Local decisions only — see the module docblock. */
|
|
147
148
|
resolution: OrgDivergenceResolutionSchema,
|
|
148
149
|
/** ISO timestamp the decision was recorded in this workspace. */
|
|
149
|
-
resolvedAt:
|
|
150
|
+
resolvedAt: IsoDateTime,
|
|
150
151
|
/**
|
|
151
152
|
* Whether anything was written back to the source system. Pinned to the
|
|
152
153
|
* literal `false`: the HRIS client is read-only and no write scope is
|
package/src/org/schemas.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - scope-check.ts (GET /api/scope/check, POST /api/scope/check-batch)
|
|
12
12
|
*/
|
|
13
13
|
import { z } from "zod";
|
|
14
|
-
import { CursorPageSchema } from "../api/primitives";
|
|
14
|
+
import { CursorPageSchema, IsoDateTime } from "../api/primitives";
|
|
15
15
|
import { OrgChartRoleSchema } from "../permissions/orgchart-roles";
|
|
16
16
|
import { PositionRefSchema } from "../identity/position-ref";
|
|
17
17
|
|
|
@@ -1757,7 +1757,7 @@ export const OwnerAuthoritySchema = z.object({
|
|
|
1757
1757
|
* the projection read filters `expires_at > now()` — so a present value is
|
|
1758
1758
|
* always in the future.
|
|
1759
1759
|
*/
|
|
1760
|
-
expiresAt:
|
|
1760
|
+
expiresAt: IsoDateTime.nullable().optional(),
|
|
1761
1761
|
});
|
|
1762
1762
|
export type OwnerAuthority = z.infer<typeof OwnerAuthoritySchema>;
|
|
1763
1763
|
|
|
@@ -1817,7 +1817,7 @@ export const CreateDelegationRequestSchema = z.object({
|
|
|
1817
1817
|
* Optional ISO-8601 expiry. Omitted or `null` ⇒ a permanent delegation. The
|
|
1818
1818
|
* UI captures a date and sends end-of-day in the user's timezone.
|
|
1819
1819
|
*/
|
|
1820
|
-
expiresAt:
|
|
1820
|
+
expiresAt: IsoDateTime.nullable().optional(),
|
|
1821
1821
|
/**
|
|
1822
1822
|
* When true, the granted user is emailed a notification (carrying `note` as
|
|
1823
1823
|
* the message). Not persisted — it only gates the side-effect email. Omitted
|
|
@@ -1836,7 +1836,7 @@ export type CreateDelegationRequest = z.infer<
|
|
|
1836
1836
|
* on an existing delegate row.
|
|
1837
1837
|
*/
|
|
1838
1838
|
export const UpdateDelegationRequestSchema = z.object({
|
|
1839
|
-
expiresAt:
|
|
1839
|
+
expiresAt: IsoDateTime.nullable(),
|
|
1840
1840
|
});
|
|
1841
1841
|
export type UpdateDelegationRequest = z.infer<
|
|
1842
1842
|
typeof UpdateDelegationRequestSchema
|
package/src/org/sync-run.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* nullable string rather than wrapped in a page envelope.
|
|
27
27
|
*/
|
|
28
28
|
import { z } from "zod";
|
|
29
|
+
import { IsoDateTime } from "../api/primitives";
|
|
29
30
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
32
|
// SyncRunSummary — the outcome record of one adapter sync run
|
|
@@ -35,9 +36,9 @@ export const SyncRunSummarySchema = z.object({
|
|
|
35
36
|
/** Unique identifier for this sync run within the org graph. */
|
|
36
37
|
id: z.string().uuid(),
|
|
37
38
|
/** ISO start of the run. Always set once a run has begun. */
|
|
38
|
-
startedAt:
|
|
39
|
+
startedAt: IsoDateTime,
|
|
39
40
|
/** ISO end of the run; `null`/absent while the run is still in progress. */
|
|
40
|
-
endedAt:
|
|
41
|
+
endedAt: IsoDateTime.nullable().optional(),
|
|
41
42
|
/** Count of facts examined by the run (the denominator). */
|
|
42
43
|
processed: z.number().int().nonnegative(),
|
|
43
44
|
/** Count of facts that resulted in a real mutation (insert/supersede). */
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* permission_decisions; the UI never re-derives.
|
|
19
19
|
*/
|
|
20
20
|
import { z } from "zod";
|
|
21
|
+
import { IsoDateTime } from "../api/primitives";
|
|
21
22
|
import { AccessLevelSchema } from "./access-levels";
|
|
22
23
|
import { AccessSourceSchema } from "./access-source";
|
|
23
24
|
import { PrincipalSchema } from "./share-api";
|
|
@@ -51,7 +52,7 @@ export const RecentDecisionSchema = z.object({
|
|
|
51
52
|
allow: z.boolean(),
|
|
52
53
|
reason: z.string(),
|
|
53
54
|
source_chain: z.array(AccessSourceSchema),
|
|
54
|
-
decided_at:
|
|
55
|
+
decided_at: IsoDateTime,
|
|
55
56
|
});
|
|
56
57
|
export type RecentDecision = z.infer<typeof RecentDecisionSchema>;
|
|
57
58
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* changing (ADR-CONTRACTS-093).
|
|
22
22
|
*/
|
|
23
23
|
import { z } from "zod";
|
|
24
|
+
import { IsoDateTime } from "../api/primitives";
|
|
24
25
|
import { AccessLevelSchema } from "./access-levels";
|
|
25
26
|
import { AccessSourceSchema } from "./access-source";
|
|
26
27
|
|
|
@@ -77,7 +78,7 @@ export const AclGrantResponseSchema = z.object({
|
|
|
77
78
|
principal: PrincipalSchema,
|
|
78
79
|
access_level: GrantableAccessLevelSchema,
|
|
79
80
|
granted_by_user_id: z.string().uuid(),
|
|
80
|
-
granted_at:
|
|
81
|
+
granted_at: IsoDateTime,
|
|
81
82
|
});
|
|
82
83
|
export type AclGrantResponse = z.infer<typeof AclGrantResponseSchema>;
|
|
83
84
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* `./secret.ts`) before storage / encryption.
|
|
14
14
|
*/
|
|
15
15
|
import { z } from "zod";
|
|
16
|
+
import { IsoDateTime } from "../api/primitives";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Usage class of an org secret. Drives which resolver layer is allowed to read
|
|
@@ -75,10 +76,10 @@ export const OrgSecretSummarySchema = z.object({
|
|
|
75
76
|
maskedPrefix: z.string().min(1).max(64),
|
|
76
77
|
version: z.number().int().positive(),
|
|
77
78
|
enabled: z.boolean(),
|
|
78
|
-
lastUsedAt:
|
|
79
|
+
lastUsedAt: IsoDateTime.nullable(),
|
|
79
80
|
usageCount: z.string(),
|
|
80
|
-
rotatedAt:
|
|
81
|
-
createdAt:
|
|
81
|
+
rotatedAt: IsoDateTime.nullable(),
|
|
82
|
+
createdAt: IsoDateTime,
|
|
82
83
|
});
|
|
83
84
|
export type OrgSecretSummary = z.infer<typeof OrgSecretSummarySchema>;
|
|
84
85
|
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* stream without first giving it a real commit-ordered log.
|
|
30
30
|
*/
|
|
31
31
|
import { z } from "zod";
|
|
32
|
+
import { IsoDateTime } from "../api/primitives";
|
|
32
33
|
import type { ResourceKey } from "../resource-keys";
|
|
33
34
|
import { isResourceKeyShape } from "../resource-keys";
|
|
34
35
|
|
|
@@ -36,7 +37,6 @@ import { isResourceKeyShape } from "../resource-keys";
|
|
|
36
37
|
* Local, not chat's. `chat/schemas.ts` owns a structurally identical helper;
|
|
37
38
|
* importing it would make a chat frame change a user-events breaking change.
|
|
38
39
|
*/
|
|
39
|
-
const IsoDateString = z.string().datetime();
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Fields every frame carries.
|
|
@@ -47,7 +47,7 @@ const IsoDateString = z.string().datetime();
|
|
|
47
47
|
*/
|
|
48
48
|
export const UserEventBaseSchema = z.object({
|
|
49
49
|
v: z.literal(1),
|
|
50
|
-
timestamp:
|
|
50
|
+
timestamp: IsoDateTime,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
// =============================================================================
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# workflows/
|
|
2
|
+
|
|
3
|
+
The registry of analysis-workflow types — the one place a domain names the
|
|
4
|
+
long-running work it hands to the durable workflow primitive.
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
An analysis workflow is a durable, domain-agnostic record of long-running AI
|
|
9
|
+
work (the backend's `analysis_workflows` table). Its lifecycle
|
|
10
|
+
(`pending | running | draining | stopped | complete | failed`) knows nothing
|
|
11
|
+
about departments, proposals or providers. The workflow's TYPE is where a
|
|
12
|
+
domain names itself, and that name travels on the wire: it is stored on the
|
|
13
|
+
workflow row, carried by the `data-workflow-progress` chat message part, and
|
|
14
|
+
answered by the generic projection read. This directory holds that vocabulary
|
|
15
|
+
so every producer, validator and renderer agrees on it.
|
|
16
|
+
|
|
17
|
+
## Invariants
|
|
18
|
+
|
|
19
|
+
- **Namespaced as `<domain>/<flow>`.** `org-structure/hris-connect` and a
|
|
20
|
+
future `org-deck/ingest` cannot collide, and neither learns the other's
|
|
21
|
+
vocabulary.
|
|
22
|
+
- **Centrally registered.** A second consumer adds one member to
|
|
23
|
+
`ANALYSIS_WORKFLOW_TYPES` and a progress adapter on its own side; it touches
|
|
24
|
+
no generic file. These are public wire discriminators, so the coupling is the
|
|
25
|
+
point — a closed const array gives wire validation and exhaustive handling.
|
|
26
|
+
- **Closed and append-only.** A member is persisted on durable rows and inside
|
|
27
|
+
immutable chat parts; removing one orphans every record that carries it.
|
|
28
|
+
- **Activity-free.** A type says WHICH flow, never what the flow is currently
|
|
29
|
+
doing. Activity is domain progress, carried opaquely by the domain.
|
|
30
|
+
- **`isWorkflowType` accepts exact members only.** A bare `hris-connect`
|
|
31
|
+
without its namespace is rejected.
|
|
32
|
+
|
|
33
|
+
## Dependencies
|
|
34
|
+
|
|
35
|
+
None. Pure vocabulary data and one pure guard; no `zod`, no runtime imports.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# workflows/\_\_tests\_\_/
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Locks the registry of analysis-workflow types and the guard that validates a
|
|
6
|
+
wire value against it.
|
|
7
|
+
|
|
8
|
+
- `types.test.ts` — pins the exact membership of `ANALYSIS_WORKFLOW_TYPES`, so
|
|
9
|
+
an append or removal is a deliberate diff here rather than a silent change
|
|
10
|
+
to a public wire discriminator. Asserts every member is namespaced as
|
|
11
|
+
`<domain>/<flow>` with exactly one separator, which is the collision guard
|
|
12
|
+
between domains. For `isWorkflowType`, the load-bearing cases are the
|
|
13
|
+
rejections: a bare `hris-connect` without its namespace, the namespace on
|
|
14
|
+
its own, a trailing separator, an unknown flow, and non-string inputs.
|
|
15
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
|
|
18
|
+
- These assert VOCABULARY and SHAPE, never behaviour. What a workflow of a
|
|
19
|
+
given type does belongs in the owning domain's suites in backend.
|
|
20
|
+
- The membership test is intentionally exact. Loosening it to "contains" would
|
|
21
|
+
let a removed member (which orphans persisted rows and immutable chat parts)
|
|
22
|
+
pass unnoticed.
|
|
23
|
+
|
|
24
|
+
## Public API
|
|
25
|
+
|
|
26
|
+
None — test-only.
|
|
27
|
+
|
|
28
|
+
## Dependencies
|
|
29
|
+
|
|
30
|
+
`vitest` and `../types`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "../types";
|
|
3
|
+
|
|
4
|
+
describe("ANALYSIS_WORKFLOW_TYPES", () => {
|
|
5
|
+
it("contains exactly the two registered org-structure flows", () => {
|
|
6
|
+
expect([...ANALYSIS_WORKFLOW_TYPES]).toEqual([
|
|
7
|
+
"org-structure/hris-connect",
|
|
8
|
+
"org-structure/reanalyse",
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("has no duplicates", () => {
|
|
13
|
+
expect(new Set(ANALYSIS_WORKFLOW_TYPES).size).toBe(
|
|
14
|
+
ANALYSIS_WORKFLOW_TYPES.length,
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Namespacing is the collision guard between domains: every member must be
|
|
19
|
+
// `<domain>/<flow>`, with exactly one separator and no empty half.
|
|
20
|
+
it("every member is namespaced as <domain>/<flow>", () => {
|
|
21
|
+
for (const type of ANALYSIS_WORKFLOW_TYPES) {
|
|
22
|
+
expect(type).toMatch(/^[a-z0-9-]+\/[a-z0-9-]+$/);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("isWorkflowType", () => {
|
|
28
|
+
it("returns true for every member of ANALYSIS_WORKFLOW_TYPES", () => {
|
|
29
|
+
for (const type of ANALYSIS_WORKFLOW_TYPES) {
|
|
30
|
+
expect(isWorkflowType(type)).toBe(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("rejects a bare flow name without its namespace", () => {
|
|
35
|
+
expect(isWorkflowType("hris-connect")).toBe(false);
|
|
36
|
+
expect(isWorkflowType("reanalyse")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects the namespace on its own and unknown flows", () => {
|
|
40
|
+
expect(isWorkflowType("org-structure")).toBe(false);
|
|
41
|
+
expect(isWorkflowType("org-structure/")).toBe(false);
|
|
42
|
+
expect(isWorkflowType("org-structure/unknown")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects non-string and empty values", () => {
|
|
46
|
+
expect(isWorkflowType("")).toBe(false);
|
|
47
|
+
expect(isWorkflowType(undefined)).toBe(false);
|
|
48
|
+
expect(isWorkflowType(null)).toBe(false);
|
|
49
|
+
expect(isWorkflowType(42)).toBe(false);
|
|
50
|
+
expect(isWorkflowType({ type: "org-structure/hris-connect" })).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis-workflow vocabulary barrel — the registered types a durable
|
|
3
|
+
* long-running analysis can be.
|
|
4
|
+
*
|
|
5
|
+
* Import from '@company-semantics/contracts' (root).
|
|
6
|
+
*
|
|
7
|
+
* @see ./types.ts for why the list is namespaced, central and closed
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type { WorkflowType } from "./types";
|
|
11
|
+
|
|
12
|
+
export { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "./types";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registered analysis-workflow types.
|
|
3
|
+
*
|
|
4
|
+
* An analysis workflow is a durable record of long-running AI work
|
|
5
|
+
* (`analysis_workflows` in the backend). Its lifecycle is domain-agnostic;
|
|
6
|
+
* its TYPE is the one place a domain names itself, and that name is a public
|
|
7
|
+
* wire discriminator: it is stored on the workflow row, carried by the
|
|
8
|
+
* `data-workflow-progress` chat message part, and returned by the generic
|
|
9
|
+
* projection read. Public wire values are validated and handled exhaustively,
|
|
10
|
+
* which is why the list is a closed const array rather than an open string.
|
|
11
|
+
*
|
|
12
|
+
* INVARIANTS:
|
|
13
|
+
* - NAMESPACED as `<domain>/<flow>`. Two domains cannot collide on a bare
|
|
14
|
+
* name: `org-structure/hris-connect` and a future `org-deck/ingest` can both
|
|
15
|
+
* exist without either learning the other's vocabulary.
|
|
16
|
+
* - CENTRALLY REGISTERED. A new consumer adds one member here and a domain
|
|
17
|
+
* progress adapter on its own side; it touches nothing generic. Central
|
|
18
|
+
* registration is the intended coupling, not an accident — the discriminator
|
|
19
|
+
* must be knowable to every renderer and validator at once.
|
|
20
|
+
* - CLOSED and append-only. A member is a persisted value on durable rows and
|
|
21
|
+
* in immutable chat parts; removing one orphans every record that carries it.
|
|
22
|
+
* - Activity-free. A type says WHICH flow; what the flow is currently doing is
|
|
23
|
+
* domain progress and never lives in this vocabulary.
|
|
24
|
+
*/
|
|
25
|
+
export const ANALYSIS_WORKFLOW_TYPES = [
|
|
26
|
+
/** Analyse an org structure freshly connected from an HRIS provider. */
|
|
27
|
+
"org-structure/hris-connect",
|
|
28
|
+
/** Re-run the org-structure analysis against an already-connected source. */
|
|
29
|
+
"org-structure/reanalyse",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
/** One registered analysis-workflow type — the wire discriminator. */
|
|
33
|
+
export type WorkflowType = (typeof ANALYSIS_WORKFLOW_TYPES)[number];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Runtime guard for an untrusted value claiming to be a {@link WorkflowType}.
|
|
37
|
+
* Accepts only exact, namespaced members; a bare flow name is rejected.
|
|
38
|
+
*/
|
|
39
|
+
export function isWorkflowType(value: unknown): value is WorkflowType {
|
|
40
|
+
return (
|
|
41
|
+
typeof value === "string" &&
|
|
42
|
+
(ANALYSIS_WORKFLOW_TYPES as readonly string[]).includes(value)
|
|
43
|
+
);
|
|
44
|
+
}
|