@company-semantics/contracts 100.0.0 → 101.1.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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +21 -7
- package/src/index.ts +6 -72
- package/src/org/capability-schemas.ts +118 -0
- package/src/org/reporting-write-schemas.ts +71 -0
- package/src/org/schemas.ts +14 -548
- package/src/org/user-org-schemas.ts +75 -0
- package/src/org/workspace-member-schemas.ts +221 -0
- package/src/org/workspace-overview-schemas.ts +92 -0
- package/src/root-exports/README.md +4 -0
- package/src/root-exports/ci-governance.ts +73 -0
- package/src/schedules/README.md +9 -0
- package/src/schedules/__tests__/api.test.ts +51 -0
- package/src/schedules/__tests__/schedules-exports.test.ts +3 -0
- package/src/schedules/api.ts +73 -0
- package/src/schedules/index.ts +7 -0
- package/src/source-sync/README.md +26 -5
- package/src/source-sync/__tests__/README.md +4 -2
- package/src/source-sync/__tests__/barrel.test.ts +8 -0
- package/src/source-sync/__tests__/scope-projection.test.ts +8 -0
- package/src/source-sync/__tests__/upkeep.test.ts +92 -1
- package/src/source-sync/index.ts +4 -0
- package/src/source-sync/schemas.ts +7 -4
- package/src/source-sync/upkeep.ts +61 -2
- package/src/source-sync/vocabulary.ts +19 -5
- package/src/workflows/__tests__/outcome.test.ts +81 -0
- package/src/workflows/deferred-route.ts +4 -0
- package/src/workflows/outcome-reasons.ts +26 -0
|
@@ -37,8 +37,10 @@ Locks the closed Source Sync vocabularies.
|
|
|
37
37
|
- `upkeep.test.ts` — locks staleness findings and standing orders: the six
|
|
38
38
|
finding kinds, the open/resolved/dismissed lifecycle, a required-but-nullable
|
|
39
39
|
dismissal, a ceiling of `none` or `low` and nothing higher, a suspended order
|
|
40
|
-
that says why,
|
|
41
|
-
grantor
|
|
40
|
+
that says why (including `grantor_lost_doc_access`), a strict standing-order
|
|
41
|
+
request that may not name its own grantor, grantability whose blocked reason
|
|
42
|
+
is non-null exactly when a scope is not grantable, and a list that always
|
|
43
|
+
carries the caller's grantability.
|
|
42
44
|
- `reach.test.ts` — locks the Drive-reach wire: reach is user or domain, the
|
|
43
45
|
probe may be unknown, `lastChange: null` parses while an absent key does
|
|
44
46
|
not, an upgrade is exactly `{ confirm: true }`, the upgrade response is the
|
|
@@ -70,6 +70,8 @@ import type {
|
|
|
70
70
|
SourceSyncReachUpgradeRequest,
|
|
71
71
|
SourceSyncReachUpgradeResponse,
|
|
72
72
|
SourceSyncStandingOrder,
|
|
73
|
+
SourceSyncStandingOrderGrantability,
|
|
74
|
+
SourceSyncStandingOrderSuspendedReason,
|
|
73
75
|
SourceSyncStandingOrdersResponse,
|
|
74
76
|
} from "../../index";
|
|
75
77
|
import * as attentionEvidence from "../attention-evidence";
|
|
@@ -162,7 +164,9 @@ const RELEASE_D_RUNTIME_NAMES: ReadonlyArray<
|
|
|
162
164
|
["SourceSyncFindingsResponseSchema", upkeep],
|
|
163
165
|
["SourceSyncDismissFindingRequestSchema", upkeep],
|
|
164
166
|
["SOURCE_SYNC_AUTO_APPLY_CEILINGS", upkeep],
|
|
167
|
+
["SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS", upkeep],
|
|
165
168
|
["SourceSyncStandingOrderSchema", upkeep],
|
|
169
|
+
["SourceSyncStandingOrderGrantabilitySchema", upkeep],
|
|
166
170
|
["SourceSyncStandingOrdersResponseSchema", upkeep],
|
|
167
171
|
["SourceSyncPutStandingOrderRequestSchema", upkeep],
|
|
168
172
|
["SOURCE_SYNC_REACHES", reach],
|
|
@@ -180,7 +184,9 @@ type ReleaseDTypes = [
|
|
|
180
184
|
SourceSyncFindingsResponse,
|
|
181
185
|
SourceSyncDismissFindingRequest,
|
|
182
186
|
SourceSyncAutoApplyCeiling,
|
|
187
|
+
SourceSyncStandingOrderSuspendedReason,
|
|
183
188
|
SourceSyncStandingOrder,
|
|
189
|
+
SourceSyncStandingOrderGrantability,
|
|
184
190
|
SourceSyncStandingOrdersResponse,
|
|
185
191
|
SourceSyncPutStandingOrderRequest,
|
|
186
192
|
SourceSyncReach,
|
|
@@ -197,7 +203,9 @@ const RELEASE_D_TYPE_NAMES = [
|
|
|
197
203
|
"SourceSyncFindingsResponse",
|
|
198
204
|
"SourceSyncDismissFindingRequest",
|
|
199
205
|
"SourceSyncAutoApplyCeiling",
|
|
206
|
+
"SourceSyncStandingOrderSuspendedReason",
|
|
200
207
|
"SourceSyncStandingOrder",
|
|
208
|
+
"SourceSyncStandingOrderGrantability",
|
|
201
209
|
"SourceSyncStandingOrdersResponse",
|
|
202
210
|
"SourceSyncPutStandingOrderRequest",
|
|
203
211
|
"SourceSyncReach",
|
|
@@ -110,6 +110,14 @@ describe("approvable and blockedReason", () => {
|
|
|
110
110
|
expect(parsed.success).toBe(true);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it("a projection may say the caller lacks approval authority", () => {
|
|
114
|
+
expect(
|
|
115
|
+
SourceSyncScopeApprovalProjectionSchema.safeParse(
|
|
116
|
+
scope({ approvable: false, blockedReason: "needs_approval_authority" }),
|
|
117
|
+
).success,
|
|
118
|
+
).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
113
121
|
it("refuses a reason outside the vocabulary and a missing approvable", () => {
|
|
114
122
|
expect(
|
|
115
123
|
SourceSyncScopeApprovalProjectionSchema.safeParse(
|
|
@@ -8,14 +8,20 @@
|
|
|
8
8
|
* - A standing order's auto-apply ceiling is none or low; nothing higher parses.
|
|
9
9
|
* - A standing-order request is strict: it may not name a grantor, a user or
|
|
10
10
|
* an actor, because the grantor is the authenticated caller.
|
|
11
|
-
* - A suspended order says why, from a closed set
|
|
11
|
+
* - A suspended order says why, from a closed set that includes the grantor
|
|
12
|
+
* losing access to the scope's document.
|
|
12
13
|
* - Money is an integer micro-USD string.
|
|
14
|
+
* - Grantability names a blocked reason exactly when a scope is not grantable,
|
|
15
|
+
* from the scope blocked-reason vocabulary.
|
|
16
|
+
* - The standing-orders list always carries the caller's grantability.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
import { describe, expect, it } from "vitest";
|
|
16
20
|
import {
|
|
17
21
|
SOURCE_SYNC_AUTO_APPLY_CEILINGS,
|
|
18
22
|
SOURCE_SYNC_FINDING_KINDS,
|
|
23
|
+
SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS,
|
|
24
|
+
SourceSyncStandingOrderGrantabilitySchema,
|
|
19
25
|
SourceSyncDismissFindingRequestSchema,
|
|
20
26
|
SourceSyncFindingSchema,
|
|
21
27
|
SourceSyncFindingsResponseSchema,
|
|
@@ -198,9 +204,94 @@ describe("standing orders", () => {
|
|
|
198
204
|
).success,
|
|
199
205
|
).toBe(false);
|
|
200
206
|
}
|
|
207
|
+
expect(
|
|
208
|
+
SourceSyncStandingOrdersResponseSchema.safeParse({
|
|
209
|
+
orders: [order()],
|
|
210
|
+
grantability: [],
|
|
211
|
+
}).success,
|
|
212
|
+
).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("a suspended order may say its grantor lost access to the document", () => {
|
|
216
|
+
expect([...SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS]).toEqual([
|
|
217
|
+
"grantor_lost_authority",
|
|
218
|
+
"grantor_lost_doc_access",
|
|
219
|
+
"spend_cap_reached",
|
|
220
|
+
"connection_lost",
|
|
221
|
+
]);
|
|
222
|
+
expect(
|
|
223
|
+
SourceSyncStandingOrderSchema.safeParse(
|
|
224
|
+
order({
|
|
225
|
+
state: "suspended",
|
|
226
|
+
suspendedReason: "grantor_lost_doc_access",
|
|
227
|
+
}),
|
|
228
|
+
).success,
|
|
229
|
+
).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("grantability names why a scope cannot be granted", () => {
|
|
233
|
+
for (const blockedReason of [
|
|
234
|
+
"needs_approval_authority",
|
|
235
|
+
"needs_doc_access",
|
|
236
|
+
]) {
|
|
237
|
+
expect(
|
|
238
|
+
SourceSyncStandingOrderGrantabilitySchema.safeParse({
|
|
239
|
+
scopeKey: "company",
|
|
240
|
+
grantable: false,
|
|
241
|
+
blockedReason,
|
|
242
|
+
}).success,
|
|
243
|
+
blockedReason,
|
|
244
|
+
).toBe(true);
|
|
245
|
+
}
|
|
246
|
+
expect(
|
|
247
|
+
SourceSyncStandingOrderGrantabilitySchema.safeParse({
|
|
248
|
+
scopeKey: "company",
|
|
249
|
+
grantable: false,
|
|
250
|
+
blockedReason: null,
|
|
251
|
+
}).success,
|
|
252
|
+
).toBe(false);
|
|
253
|
+
expect(
|
|
254
|
+
SourceSyncStandingOrderGrantabilitySchema.safeParse({
|
|
255
|
+
scopeKey: "company",
|
|
256
|
+
grantable: false,
|
|
257
|
+
blockedReason: "felt_like_it",
|
|
258
|
+
}).success,
|
|
259
|
+
).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("a grantable scope carries no blocked reason", () => {
|
|
263
|
+
expect(
|
|
264
|
+
SourceSyncStandingOrderGrantabilitySchema.safeParse({
|
|
265
|
+
scopeKey: "company",
|
|
266
|
+
grantable: true,
|
|
267
|
+
blockedReason: null,
|
|
268
|
+
}).success,
|
|
269
|
+
).toBe(true);
|
|
270
|
+
expect(
|
|
271
|
+
SourceSyncStandingOrderGrantabilitySchema.safeParse({
|
|
272
|
+
scopeKey: "company",
|
|
273
|
+
grantable: true,
|
|
274
|
+
blockedReason: "needs_doc_access",
|
|
275
|
+
}).success,
|
|
276
|
+
).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("the standing-orders list carries the caller's grantability", () => {
|
|
201
280
|
expect(
|
|
202
281
|
SourceSyncStandingOrdersResponseSchema.safeParse({ orders: [order()] })
|
|
203
282
|
.success,
|
|
283
|
+
).toBe(false);
|
|
284
|
+
expect(
|
|
285
|
+
SourceSyncStandingOrdersResponseSchema.safeParse({
|
|
286
|
+
orders: [order()],
|
|
287
|
+
grantability: [
|
|
288
|
+
{
|
|
289
|
+
scopeKey: "company",
|
|
290
|
+
grantable: false,
|
|
291
|
+
blockedReason: "needs_approval_authority",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
}).success,
|
|
204
295
|
).toBe(true);
|
|
205
296
|
});
|
|
206
297
|
});
|
package/src/source-sync/index.ts
CHANGED
|
@@ -192,7 +192,9 @@ export type {
|
|
|
192
192
|
SourceSyncFindingsResponse,
|
|
193
193
|
SourceSyncDismissFindingRequest,
|
|
194
194
|
SourceSyncAutoApplyCeiling,
|
|
195
|
+
SourceSyncStandingOrderSuspendedReason,
|
|
195
196
|
SourceSyncStandingOrder,
|
|
197
|
+
SourceSyncStandingOrderGrantability,
|
|
196
198
|
SourceSyncStandingOrdersResponse,
|
|
197
199
|
SourceSyncPutStandingOrderRequest,
|
|
198
200
|
} from "./upkeep";
|
|
@@ -203,7 +205,9 @@ export {
|
|
|
203
205
|
SourceSyncFindingsResponseSchema,
|
|
204
206
|
SourceSyncDismissFindingRequestSchema,
|
|
205
207
|
SOURCE_SYNC_AUTO_APPLY_CEILINGS,
|
|
208
|
+
SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS,
|
|
206
209
|
SourceSyncStandingOrderSchema,
|
|
210
|
+
SourceSyncStandingOrderGrantabilitySchema,
|
|
207
211
|
SourceSyncStandingOrdersResponseSchema,
|
|
208
212
|
SourceSyncPutStandingOrderRequestSchema,
|
|
209
213
|
} from "./upkeep";
|
|
@@ -47,10 +47,13 @@ const PersonLabel = z.string().min(1).max(255);
|
|
|
47
47
|
* - An approved scope reports the routing its approval RECORDED; a later head
|
|
48
48
|
* change does not rewrite it. A pending scope reports its routing as of now.
|
|
49
49
|
* - `approvable` is whether THE CALLER may approve this pending scope now. A scope
|
|
50
|
-
* can be visible but not approvable: approving needs
|
|
51
|
-
*
|
|
52
|
-
* approver. `blockedReason` is non-null
|
|
53
|
-
*
|
|
50
|
+
* can be visible but not approvable: approving needs approval authority on the
|
|
51
|
+
* scope AND the caller's own upload and edit access on the scope's document,
|
|
52
|
+
* because landing and apply act as the approver. `blockedReason` is non-null
|
|
53
|
+
* exactly when a pending scope is not approvable BY THE CALLER:
|
|
54
|
+
* `needs_approval_authority` (no approval authority: e.g. an org admin, who
|
|
55
|
+
* coordinates and never approves) or `needs_doc_access`. An approved scope is
|
|
56
|
+
* never approvable and carries null. At the
|
|
54
57
|
* synthesis checkpoint a pending scope is approvable: apply is decided per
|
|
55
58
|
* document by the edit policy, which approving the candidate set required.
|
|
56
59
|
*/
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import { IsoDateTime } from "../api/primitives";
|
|
21
|
+
import { SOURCE_SYNC_SCOPE_BLOCKED_REASONS } from "./vocabulary";
|
|
21
22
|
|
|
22
23
|
export const SOURCE_SYNC_FINDING_KINDS = [
|
|
23
24
|
"source_changed",
|
|
@@ -92,6 +93,29 @@ export type SourceSyncAutoApplyCeiling =
|
|
|
92
93
|
|
|
93
94
|
const MicroUsd = z.string().regex(/^[0-9]+$/);
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Why a standing order stopped acting. Closed; mirrors the backend
|
|
98
|
+
* `source_sync_standing_orders_suspended_reason_valid` CHECK exactly.
|
|
99
|
+
*
|
|
100
|
+
* - `grantor_lost_authority`: the grantor no longer holds approval authority on
|
|
101
|
+
* the scope (they left the org, lost the unit, or were an org admin, who no
|
|
102
|
+
* longer approves).
|
|
103
|
+
* - `grantor_lost_doc_access`: the grantor still holds the authority but can no
|
|
104
|
+
* longer upload to and edit the scope's document, so an unattended landing or
|
|
105
|
+
* apply would be refused as them. Nothing is retried as anyone else.
|
|
106
|
+
* - `spend_cap_reached`: the month's spend met the cap the grantor stated.
|
|
107
|
+
* - `connection_lost`: Google Drive was disconnected; a reconnect re-activates
|
|
108
|
+
* nothing.
|
|
109
|
+
*/
|
|
110
|
+
export const SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS = [
|
|
111
|
+
"grantor_lost_authority",
|
|
112
|
+
"grantor_lost_doc_access",
|
|
113
|
+
"spend_cap_reached",
|
|
114
|
+
"connection_lost",
|
|
115
|
+
] as const;
|
|
116
|
+
export type SourceSyncStandingOrderSuspendedReason =
|
|
117
|
+
(typeof SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS)[number];
|
|
118
|
+
|
|
95
119
|
export const SourceSyncStandingOrderSchema = z.object({
|
|
96
120
|
/** 'company' or 'unit:<uuid>'. */
|
|
97
121
|
scopeKey: z.string().min(1).max(64),
|
|
@@ -101,14 +125,46 @@ export const SourceSyncStandingOrderSchema = z.object({
|
|
|
101
125
|
/** Display-only, resolved at read time. */
|
|
102
126
|
grantedByLabel: z.string().max(255).nullable(),
|
|
103
127
|
grantedAt: IsoDateTime,
|
|
104
|
-
/** Why it stopped acting
|
|
128
|
+
/** Why it stopped acting; null unless suspended. */
|
|
105
129
|
suspendedReason: z
|
|
106
|
-
.enum(
|
|
130
|
+
.enum(SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS)
|
|
107
131
|
.nullable(),
|
|
108
132
|
});
|
|
109
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether the CALLER may grant (turn on, change or turn on again) the standing
|
|
136
|
+
* order of one scope, decided by the server with exactly the rule a grant
|
|
137
|
+
* enforces: approval authority on the scope (an account owner, or authority on
|
|
138
|
+
* that unit) AND the caller's own upload and edit access on the scope's
|
|
139
|
+
* document. Authority is checked first: `needs_approval_authority` when the
|
|
140
|
+
* caller holds none (an org admin coordinates, never approves);
|
|
141
|
+
* `needs_doc_access` when they hold it but cannot act on the document.
|
|
142
|
+
* `blockedReason` is non-null exactly when `grantable` is false. Advisory: the
|
|
143
|
+
* grant re-decides and refuses (403) whatever this said.
|
|
144
|
+
*/
|
|
145
|
+
export const SourceSyncStandingOrderGrantabilitySchema = z
|
|
146
|
+
.object({
|
|
147
|
+
/** 'company' or 'unit:<uuid>'. */
|
|
148
|
+
scopeKey: z.string().min(1).max(64),
|
|
149
|
+
grantable: z.boolean(),
|
|
150
|
+
blockedReason: z.enum(SOURCE_SYNC_SCOPE_BLOCKED_REASONS).nullable(),
|
|
151
|
+
})
|
|
152
|
+
.strict()
|
|
153
|
+
.refine((g) => g.grantable === (g.blockedReason === null), {
|
|
154
|
+
message:
|
|
155
|
+
"blockedReason is non-null exactly when the scope is not grantable",
|
|
156
|
+
path: ["blockedReason"],
|
|
157
|
+
});
|
|
158
|
+
|
|
110
159
|
export const SourceSyncStandingOrdersResponseSchema = z.object({
|
|
111
160
|
orders: z.array(SourceSyncStandingOrderSchema).max(500),
|
|
161
|
+
/**
|
|
162
|
+
* The caller's grantability: the company scope first, then each distinct
|
|
163
|
+
* scope of an active or suspended order in `orders`, at most once each. A
|
|
164
|
+
* scope absent here is undecided (the server evaluated a bounded number); the
|
|
165
|
+
* grant still decides.
|
|
166
|
+
*/
|
|
167
|
+
grantability: z.array(SourceSyncStandingOrderGrantabilitySchema).max(500),
|
|
112
168
|
});
|
|
113
169
|
|
|
114
170
|
/**
|
|
@@ -132,6 +188,9 @@ export type SourceSyncDismissFindingRequest = z.infer<
|
|
|
132
188
|
export type SourceSyncStandingOrder = z.infer<
|
|
133
189
|
typeof SourceSyncStandingOrderSchema
|
|
134
190
|
>;
|
|
191
|
+
export type SourceSyncStandingOrderGrantability = z.infer<
|
|
192
|
+
typeof SourceSyncStandingOrderGrantabilitySchema
|
|
193
|
+
>;
|
|
135
194
|
export type SourceSyncStandingOrdersResponse = z.infer<
|
|
136
195
|
typeof SourceSyncStandingOrdersResponseSchema
|
|
137
196
|
>;
|
|
@@ -112,12 +112,26 @@ export type SourceSyncScopeApproverKind =
|
|
|
112
112
|
(typeof SOURCE_SYNC_SCOPE_APPROVER_KINDS)[number];
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Why the CALLER may not approve a scope they can see
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
115
|
+
* Why the CALLER may not approve a scope they can see, or grant its standing
|
|
116
|
+
* order. One vocabulary for both decisions (they are the same rule).
|
|
117
|
+
*
|
|
118
|
+
* - `needs_approval_authority`: the caller may see the scope but holds no
|
|
119
|
+
* approval authority on it. Approval authority is an account owner, or
|
|
120
|
+
* authority on that unit (local, inherited or delegated). An org admin
|
|
121
|
+
* coordinates and never approves: being responsible for work getting done is
|
|
122
|
+
* not authority to do it.
|
|
123
|
+
* - `needs_doc_access`: the caller holds the authority but lacks their own
|
|
124
|
+
* upload and edit access on the scope's document; landing and apply act as
|
|
125
|
+
* the approver (or a standing order's grantor), so approving would land
|
|
126
|
+
* nothing.
|
|
127
|
+
*
|
|
128
|
+
* Authority is checked first, so a caller lacking both reads
|
|
129
|
+
* `needs_approval_authority`.
|
|
119
130
|
*/
|
|
120
|
-
export const SOURCE_SYNC_SCOPE_BLOCKED_REASONS = [
|
|
131
|
+
export const SOURCE_SYNC_SCOPE_BLOCKED_REASONS = [
|
|
132
|
+
"needs_approval_authority",
|
|
133
|
+
"needs_doc_access",
|
|
134
|
+
] as const;
|
|
121
135
|
export type SourceSyncScopeBlockedReason =
|
|
122
136
|
(typeof SOURCE_SYNC_SCOPE_BLOCKED_REASONS)[number];
|
|
123
137
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ORG_STRUCTURE_OUTCOME_KIND_OF_REASON,
|
|
14
14
|
SYNTHESIZE_AFFORDANCES_OF_REASON,
|
|
15
15
|
SYNTHESIZE_OUTCOME_KIND_OF_REASON,
|
|
16
|
+
SynthesizeOutcomeFactsSchema,
|
|
16
17
|
WORKFLOW_OUTCOME_KINDS,
|
|
17
18
|
} from "../outcome-reasons";
|
|
18
19
|
|
|
@@ -193,3 +194,83 @@ describe("WorkflowOutcome", () => {
|
|
|
193
194
|
).toBe(false);
|
|
194
195
|
});
|
|
195
196
|
});
|
|
197
|
+
|
|
198
|
+
describe("source-sync/synthesize unattended endings", () => {
|
|
199
|
+
// The strict member (src/workflows/outcome.ts memberSchema): workflowType,
|
|
200
|
+
// executionEpoch, kind, reason, facts, disposition, affordances. Each test
|
|
201
|
+
// below supplies kind, reason, facts and its `review` disposition.
|
|
202
|
+
const base = {
|
|
203
|
+
workflowType: "source-sync/synthesize",
|
|
204
|
+
executionEpoch: 0,
|
|
205
|
+
affordances: [],
|
|
206
|
+
};
|
|
207
|
+
const unattended = {
|
|
208
|
+
scopeCount: 1,
|
|
209
|
+
violationCount: 0,
|
|
210
|
+
notLandedCount: 0,
|
|
211
|
+
appliedDocCount: 1,
|
|
212
|
+
notAppliedDocCount: 1,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
it("a partly applied unattended batch is partial and may hand off to review", () => {
|
|
216
|
+
expect(SYNTHESIZE_OUTCOME_KIND_OF_REASON.updates_applied_partial).toBe(
|
|
217
|
+
"partial",
|
|
218
|
+
);
|
|
219
|
+
expect(SYNTHESIZE_OUTCOME_KIND_OF_REASON.apply_refused).toBe("partial");
|
|
220
|
+
expect(
|
|
221
|
+
WorkflowOutcomeSchema.safeParse({
|
|
222
|
+
...base,
|
|
223
|
+
kind: "partial",
|
|
224
|
+
reason: "updates_applied_partial",
|
|
225
|
+
facts: unattended,
|
|
226
|
+
disposition: {
|
|
227
|
+
kind: "review",
|
|
228
|
+
requestType: "source-sync/synthesis",
|
|
229
|
+
requestId: "batch-1",
|
|
230
|
+
},
|
|
231
|
+
}).success,
|
|
232
|
+
).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("an unattended ending names how many documents it applied", () => {
|
|
236
|
+
const { appliedDocCount: _a, ...withoutApplied } = unattended;
|
|
237
|
+
expect(
|
|
238
|
+
WorkflowOutcomeSchema.safeParse({
|
|
239
|
+
...base,
|
|
240
|
+
kind: "partial",
|
|
241
|
+
reason: "apply_refused",
|
|
242
|
+
facts: withoutApplied,
|
|
243
|
+
disposition: {
|
|
244
|
+
kind: "review",
|
|
245
|
+
requestType: "source-sync/synthesis",
|
|
246
|
+
requestId: "batch-1",
|
|
247
|
+
},
|
|
248
|
+
}).success,
|
|
249
|
+
).toBe(false);
|
|
250
|
+
expect(
|
|
251
|
+
WorkflowOutcomeSchema.safeParse({
|
|
252
|
+
...base,
|
|
253
|
+
kind: "partial",
|
|
254
|
+
reason: "apply_refused",
|
|
255
|
+
facts: { ...unattended, appliedDocCount: 0, notAppliedDocCount: 2 },
|
|
256
|
+
disposition: {
|
|
257
|
+
kind: "review",
|
|
258
|
+
requestType: "source-sync/synthesis",
|
|
259
|
+
requestId: "batch-1",
|
|
260
|
+
},
|
|
261
|
+
}).success,
|
|
262
|
+
).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("an attended ending says no automatic apply happened with null counts", () => {
|
|
266
|
+
expect(
|
|
267
|
+
SynthesizeOutcomeFactsSchema.safeParse({
|
|
268
|
+
scopeCount: 1,
|
|
269
|
+
violationCount: 0,
|
|
270
|
+
notLandedCount: 0,
|
|
271
|
+
appliedDocCount: null,
|
|
272
|
+
notAppliedDocCount: null,
|
|
273
|
+
}).success,
|
|
274
|
+
).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -150,6 +150,10 @@ export const SYNTHESIZE_DEFERRED_ROUTE_OF_REASON = {
|
|
|
150
150
|
updates_ready_partial: { route: "covered", by: "sourceSync.review_ready", fallback: "notice", audience: "creator" },
|
|
151
151
|
// prettier-ignore
|
|
152
152
|
updates_applied: { route: "covered", by: "sourceSync.docs_updated", fallback: "notice", audience: "creator" },
|
|
153
|
+
// prettier-ignore
|
|
154
|
+
updates_applied_partial: { route: "covered", by: "sourceSync.review_ready", fallback: "notice", audience: "creator" },
|
|
155
|
+
// prettier-ignore
|
|
156
|
+
apply_refused: { route: "covered", by: "sourceSync.review_ready", fallback: "notice", audience: "creator" },
|
|
153
157
|
stopped_by_user: STOPPED,
|
|
154
158
|
paused_billing: EXPLAINED,
|
|
155
159
|
run_ceiling_reached: NOTICE_ADMINS,
|
|
@@ -122,7 +122,19 @@ export const SYNTHESIZE_OUTCOME_KIND_OF_REASON = {
|
|
|
122
122
|
updates_ready: "produced",
|
|
123
123
|
/** Drafted from what landed; some approved files could not be brought in. */
|
|
124
124
|
updates_ready_partial: "partial",
|
|
125
|
+
/** An unattended batch: the standing order applied EVERY drafted document. */
|
|
125
126
|
updates_applied: "produced",
|
|
127
|
+
/**
|
|
128
|
+
* An unattended batch: the standing order applied some documents; the rest
|
|
129
|
+
* (proposed by the auto-apply gate, or refused when applied) wait for a person.
|
|
130
|
+
*/
|
|
131
|
+
updates_applied_partial: "partial",
|
|
132
|
+
/**
|
|
133
|
+
* An unattended batch: the automatic apply was attempted and refused for every
|
|
134
|
+
* document it tried, so nothing was applied; the drafts wait for review. Never
|
|
135
|
+
* narrated as applied.
|
|
136
|
+
*/
|
|
137
|
+
apply_refused: "partial",
|
|
126
138
|
stopped_by_user: "stopped",
|
|
127
139
|
paused_billing: "failed",
|
|
128
140
|
run_ceiling_reached: "failed",
|
|
@@ -146,6 +158,8 @@ export const SYNTHESIZE_AFFORDANCES_OF_REASON = {
|
|
|
146
158
|
updates_ready: [],
|
|
147
159
|
updates_ready_partial: [],
|
|
148
160
|
updates_applied: [],
|
|
161
|
+
updates_applied_partial: [],
|
|
162
|
+
apply_refused: [],
|
|
149
163
|
stopped_by_user: [],
|
|
150
164
|
paused_billing: [OPEN_BILLING],
|
|
151
165
|
run_ceiling_reached: [],
|
|
@@ -173,6 +187,18 @@ export const SynthesizeOutcomeFactsSchema = z
|
|
|
173
187
|
violationCount: Count,
|
|
174
188
|
/** Approved files to attach that did not land (refused, unreadable, gone). */
|
|
175
189
|
notLandedCount: Count,
|
|
190
|
+
/**
|
|
191
|
+
* Documents an UNATTENDED batch applied on its own (the standing order's
|
|
192
|
+
* auto-apply). Null when no automatic apply happened at all: an attended
|
|
193
|
+
* run, or an unattended run that ended before its batch validated. Missing
|
|
194
|
+
* evidence is null, never 0.
|
|
195
|
+
*/
|
|
196
|
+
appliedDocCount: Count.nullable(),
|
|
197
|
+
/**
|
|
198
|
+
* Documents of an UNATTENDED batch left for a person (proposed by the gate,
|
|
199
|
+
* or refused when applied). Null exactly when `appliedDocCount` is null.
|
|
200
|
+
*/
|
|
201
|
+
notAppliedDocCount: Count.nullable(),
|
|
176
202
|
})
|
|
177
203
|
.strict();
|
|
178
204
|
|