@company-semantics/contracts 99.3.0 → 101.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@ export type {
31
31
  SourceSyncDocDisposition,
32
32
  SourceSyncScopeApprovalState,
33
33
  SourceSyncScopeApproverKind,
34
+ SourceSyncScopeBlockedReason,
34
35
  DocumentType,
35
36
  KnownDocumentType,
36
37
  OperatingKnowledgeFamily,
@@ -47,6 +48,7 @@ export {
47
48
  SOURCE_SYNC_DOC_DISPOSITIONS,
48
49
  SOURCE_SYNC_SCOPE_APPROVAL_STATES,
49
50
  SOURCE_SYNC_SCOPE_APPROVER_KINDS,
51
+ SOURCE_SYNC_SCOPE_BLOCKED_REASONS,
50
52
  DOCUMENT_TYPES,
51
53
  OPERATING_KNOWLEDGE_FAMILIES,
52
54
  } from "./vocabulary";
@@ -190,7 +192,9 @@ export type {
190
192
  SourceSyncFindingsResponse,
191
193
  SourceSyncDismissFindingRequest,
192
194
  SourceSyncAutoApplyCeiling,
195
+ SourceSyncStandingOrderSuspendedReason,
193
196
  SourceSyncStandingOrder,
197
+ SourceSyncStandingOrderGrantability,
194
198
  SourceSyncStandingOrdersResponse,
195
199
  SourceSyncPutStandingOrderRequest,
196
200
  } from "./upkeep";
@@ -201,7 +205,9 @@ export {
201
205
  SourceSyncFindingsResponseSchema,
202
206
  SourceSyncDismissFindingRequestSchema,
203
207
  SOURCE_SYNC_AUTO_APPLY_CEILINGS,
208
+ SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS,
204
209
  SourceSyncStandingOrderSchema,
210
+ SourceSyncStandingOrderGrantabilitySchema,
205
211
  SourceSyncStandingOrdersResponseSchema,
206
212
  SourceSyncPutStandingOrderRequestSchema,
207
213
  } from "./upkeep";
@@ -26,6 +26,7 @@ import {
26
26
  SOURCE_SYNC_DOC_RISKS,
27
27
  SOURCE_SYNC_SCOPE_APPROVAL_STATES,
28
28
  SOURCE_SYNC_SCOPE_APPROVER_KINDS,
29
+ SOURCE_SYNC_SCOPE_BLOCKED_REASONS,
29
30
  } from "./vocabulary";
30
31
  import { CandidateDiscoverySummarySchema } from "./relevance";
31
32
  import { SourceSyncCandidateItemSchema } from "./candidate-item";
@@ -45,6 +46,16 @@ const PersonLabel = z.string().min(1).max(255);
45
46
  * still a `unit_head` scope, `assignedTo` is null, and the org's admins approve it.
46
47
  * - An approved scope reports the routing its approval RECORDED; a later head
47
48
  * change does not rewrite it. A pending scope reports its routing as of now.
49
+ * - `approvable` is whether THE CALLER may approve this pending scope now. A scope
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
57
+ * synthesis checkpoint a pending scope is approvable: apply is decided per
58
+ * document by the edit policy, which approving the candidate set required.
48
59
  */
49
60
  export const SourceSyncScopeApprovalProjectionSchema = z.object({
50
61
  /** 'company' or 'unit:<uuid>'. */
@@ -62,6 +73,8 @@ export const SourceSyncScopeApprovalProjectionSchema = z.object({
62
73
  /** Non-null exactly when a person's approval is recorded. */
63
74
  approvedBy: z.object({ label: PersonLabel }).nullable(),
64
75
  approvedAt: IsoDateTime.nullable(),
76
+ approvable: z.boolean(),
77
+ blockedReason: z.enum(SOURCE_SYNC_SCOPE_BLOCKED_REASONS).nullable(),
65
78
  });
66
79
 
67
80
  /** The discovery run a person started, or that upkeep started on a schedule. */
@@ -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
  >;
@@ -111,6 +111,30 @@ export const SOURCE_SYNC_SCOPE_APPROVER_KINDS = [
111
111
  export type SourceSyncScopeApproverKind =
112
112
  (typeof SOURCE_SYNC_SCOPE_APPROVER_KINDS)[number];
113
113
 
114
+ /**
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`.
130
+ */
131
+ export const SOURCE_SYNC_SCOPE_BLOCKED_REASONS = [
132
+ "needs_approval_authority",
133
+ "needs_doc_access",
134
+ ] as const;
135
+ export type SourceSyncScopeBlockedReason =
136
+ (typeof SOURCE_SYNC_SCOPE_BLOCKED_REASONS)[number];
137
+
114
138
  /**
115
139
  * What a source IS — a STRUCTURAL name the model observes, never a relevance verdict
116
140
  * (control ADR source-discovery-observation-vs-policy). The set names operating
@@ -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
+ });
@@ -147,7 +147,13 @@ export const SYNTHESIZE_DEFERRED_ROUTE_OF_REASON = {
147
147
  // prettier-ignore
148
148
  updates_ready: { route: "covered", by: "sourceSync.review_ready", fallback: "notice", audience: "creator" },
149
149
  // prettier-ignore
150
+ updates_ready_partial: { route: "covered", by: "sourceSync.review_ready", fallback: "notice", audience: "creator" },
151
+ // prettier-ignore
150
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" },
151
157
  stopped_by_user: STOPPED,
152
158
  paused_billing: EXPLAINED,
153
159
  run_ceiling_reached: NOTICE_ADMINS,
@@ -120,7 +120,21 @@ export type DiscoverOutcomeFacts = z.infer<typeof DiscoverOutcomeFactsSchema>;
120
120
  /** Why a synthesis (landing then drafting) run ended the way it did. */
121
121
  export const SYNTHESIZE_OUTCOME_KIND_OF_REASON = {
122
122
  updates_ready: "produced",
123
+ /** Drafted from what landed; some approved files could not be brought in. */
124
+ updates_ready_partial: "partial",
125
+ /** An unattended batch: the standing order applied EVERY drafted document. */
123
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",
124
138
  stopped_by_user: "stopped",
125
139
  paused_billing: "failed",
126
140
  run_ceiling_reached: "failed",
@@ -142,7 +156,10 @@ export type SynthesizeOutcomeReason =
142
156
  /** What each synthesis ending offers. TOTAL; an empty list is explicit. */
143
157
  export const SYNTHESIZE_AFFORDANCES_OF_REASON = {
144
158
  updates_ready: [],
159
+ updates_ready_partial: [],
145
160
  updates_applied: [],
161
+ updates_applied_partial: [],
162
+ apply_refused: [],
146
163
  stopped_by_user: [],
147
164
  paused_billing: [OPEN_BILLING],
148
165
  run_ceiling_reached: [],
@@ -168,6 +185,20 @@ export const SynthesizeOutcomeFactsSchema = z
168
185
  scopeCount: Count,
169
186
  /** Authority or size violations that left the batch incomplete. */
170
187
  violationCount: Count,
188
+ /** Approved files to attach that did not land (refused, unreadable, gone). */
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(),
171
202
  })
172
203
  .strict();
173
204