@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.
@@ -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, and a strict standing-order request that may not name its own
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
  });
@@ -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 the caller's own upload and
51
- * edit access on the scope's document, because landing and apply act as the
52
- * approver. `blockedReason` is non-null exactly when a pending scope is not
53
- * approvable; an approved scope is never approvable and carries null. At the
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, e.g. the grantor lost the authority it was granted under. */
128
+ /** Why it stopped acting; null unless suspended. */
105
129
  suspendedReason: z
106
- .enum(["grantor_lost_authority", "spend_cap_reached", "connection_lost"])
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. `needs_doc_access`: the
116
- * landing and the apply act as the approver, so approving needs the caller's own
117
- * upload and edit access on the scope's document (an admin holds only metadata
118
- * access to company.md content).
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 = ["needs_doc_access"] as const;
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