@company-semantics/contracts 100.0.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.
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Workspace Member Schemas
3
+ *
4
+ * Zod schemas for the workspace member list and detail, the role catalog, and
5
+ * the member mutation responses (GET /api/workspace/members, DELETE/PATCH
6
+ * /api/workspace/members/:id). Re-exported from `./schemas`, the one import
7
+ * path for every org schema.
8
+ */
9
+ import { z } from "zod";
10
+ import { CursorPageSchema } from "../api/primitives";
11
+ import { OrgChartRoleSchema } from "../permissions/orgchart-roles";
12
+ import { AuthorityMutabilitySchema } from "./authority-schemas";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Sub-schemas
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const WorkspaceMemberUnitSummarySchema = z.object({
19
+ unitId: z.string(),
20
+ unitName: z.string(),
21
+ unitPath: z.string(),
22
+ role: z.enum(["owner", "manager", "member"]),
23
+ });
24
+
25
+ /**
26
+ * Designation a member holds over a unit they manage (ADR-CTRL-112).
27
+ *
28
+ * Precedence, highest-first: whole_org > admin > unit_owner > delegate > member.
29
+ * The org-wide designations (`whole_org`, `admin`) anchor to the org root
30
+ * unit. This vocabulary is DISPLAY-ONLY — it summarizes management reach for
31
+ * UI surfaces and is NEVER an authorization input (authority resolution uses
32
+ * the orthogonal membership/delegation axes, not this enum).
33
+ */
34
+ export const OrgUnitDesignationSchema = z.enum([
35
+ "whole_org",
36
+ "admin",
37
+ "unit_owner",
38
+ "delegate",
39
+ "member",
40
+ ]);
41
+ export type OrgUnitDesignation = z.infer<typeof OrgUnitDesignationSchema>;
42
+
43
+ export const WorkspaceMemberUnitDesignationEntrySchema = z.object({
44
+ unitId: z.string(),
45
+ unitName: z.string(),
46
+ designation: OrgUnitDesignationSchema,
47
+ /**
48
+ * Whether this designation is REMOVABLE in place (`user_managed` — an explicit
49
+ * overlay grant or `l{N}_unit_owner` membership) or DERIVED from reporting
50
+ * topology (`derived` — the senior person homed in a unit with no manager
51
+ * inside it, ADR-BE-267/294). A `derived` owner can't be revoked: the holder
52
+ * must relinquish it (recorded as a higher-provenance suppression that the
53
+ * structural projection honors) or the upstream chart must change. Display +
54
+ * affordance gating only; never an authorization input.
55
+ */
56
+ mutability: AuthorityMutabilitySchema,
57
+ });
58
+ export type WorkspaceMemberUnitDesignationEntry = z.infer<
59
+ typeof WorkspaceMemberUnitDesignationEntrySchema
60
+ >;
61
+
62
+ const WorkspaceMemberSchema = z.object({
63
+ id: z.string(),
64
+ name: z.string(),
65
+ email: z.string(),
66
+ /**
67
+ * Resolved avatar photo URL for the member, or `null`/absent when none is
68
+ * stored. SOURCE-AGNOSTIC: today the backend resolves this from the Slack
69
+ * profile photo cached on the user record, but the field name deliberately
70
+ * does not encode the origin so future sources (in-app upload, etc.) slot in
71
+ * behind the backend avatar resolver without a contract change. UI MUST fall
72
+ * back to initials when this is absent. Optional because it is an additive
73
+ * display-only enrichment (see ADR-CONTRACTS-055).
74
+ */
75
+ avatarUrl: z.string().nullable().optional(),
76
+ /** The occupant's Position seat title (org-scoped). Null when the seat is untitled. */
77
+ jobTitle: z.string().nullable(),
78
+ /**
79
+ * The member's Position seat in the org graph — the id every reporting write
80
+ * is keyed on (ADR-CONTRACTS-131). Present here so a user-ADDRESSED surface
81
+ * (the members table, a person picker) can resolve a seat by READ and hand
82
+ * the position straight to `PUT /api/reporting/positions/{positionId}/manager`,
83
+ * instead of a write path minting one with `resolveSeat(create: true)`.
84
+ *
85
+ * Nullable: a member with no materialized seat is a legitimate state, and a
86
+ * read must report that honestly rather than force the server to create one.
87
+ */
88
+ seatPositionId: z.string().uuid().nullable(),
89
+ /**
90
+ * Display role — the member's highest org-chart standing, or `null` for a
91
+ * plain member with no org-chart authority. Render via `orgChartRoleLabel`.
92
+ */
93
+ role: OrgChartRoleSchema.nullable(),
94
+ /** Raw RBAC role names (e.g. 'owner', 'admin', 'delegate'). Superset of `role`. */
95
+ roleNames: z.array(z.string()),
96
+ joinedAt: z.string(),
97
+ /** ISO timestamp of last activity; null if never recorded. */
98
+ lastActiveAt: z.string().nullable(),
99
+ /**
100
+ * Home org-unit (`users.primary_unit_id`). Null for org-scoped users
101
+ * (no level-2 home assignment). Used by the unit-detail Members table to
102
+ * roll up "people in this org-unit" across the subtree.
103
+ */
104
+ primaryUnitId: z.string().uuid().nullable(),
105
+ /** OrgUnit memberships for this member, capped at a small number server-side. */
106
+ unitMemberships: z.array(WorkspaceMemberUnitSummarySchema),
107
+ /**
108
+ * Per-unit org-chart designations the member holds (ADR-CTRL-112). Carries
109
+ * BOTH management-reach entries (whole_org/admin/unit_owner/delegate) and
110
+ * plain contributor entries (`member`); consumers split by designation.
111
+ * Display-only summary; never an authorization input. Empty array when the
112
+ * member holds no per-unit designation.
113
+ */
114
+ unitDesignations: z.array(WorkspaceMemberUnitDesignationEntrySchema),
115
+ /** True when server truncated `unitMemberships`; detail endpoint returns the full list. */
116
+ unitMembershipsTruncated: z.boolean(),
117
+ /** Invite lifecycle status. `null` for members without an invite record. */
118
+ inviteStatus: z.enum(["active", "pending", "expired"]).nullable(),
119
+ });
120
+
121
+ const MemberRecentActionSchema = z.object({
122
+ id: z.string(),
123
+ timestamp: z.string(),
124
+ action: z.string(),
125
+ summary: z.string(),
126
+ });
127
+
128
+ /**
129
+ * Which bundle or role contributed a resolved scope (ADR-CTRL-360).
130
+ *
131
+ * `member_default` / `admin` / `account_owner` are the static actor-class
132
+ * bundles AUTH-011C re-homed the deleted role scope lists onto (ADR-BE-184);
133
+ * `role:<name>` is a surviving RBAC role assignment. Open string rather than an
134
+ * enum because `rbac_roles` is data, not a closed vocabulary.
135
+ *
136
+ * A scope carries EVERY band that granted it — composition is a union, not a
137
+ * ladder, so an owner who also holds admin authority legitimately shows both.
138
+ * Display/audit only; `effectiveScopes` remains the authorization surface.
139
+ */
140
+ const ScopeProvenanceEntrySchema = z.object({
141
+ scope: z.string(),
142
+ bands: z.array(z.string()).min(1),
143
+ });
144
+
145
+ export const WorkspaceMemberDetailSchema = WorkspaceMemberSchema.extend({
146
+ effectiveScopes: z.array(z.string()),
147
+ /**
148
+ * Per-scope provenance for {@link WorkspaceMemberDetailSchema.shape.effectiveScopes}.
149
+ * Optional: absence means the server did not compute it (legacy callsite, or a
150
+ * non-user actor), never that the scopes were granted by nothing.
151
+ */
152
+ scopeProvenance: z.array(ScopeProvenanceEntrySchema).optional(),
153
+ recentActions: z.array(MemberRecentActionSchema),
154
+ });
155
+
156
+ /**
157
+ * Workspace member for the members list. Human users only (no agent actors).
158
+ * Canonical type — inferred from {@link WorkspaceMemberSchema} so the runtime
159
+ * schema is the single source of truth (no parallel hand-written interface).
160
+ */
161
+ export type WorkspaceMember = z.infer<typeof WorkspaceMemberSchema>;
162
+
163
+ /**
164
+ * Full member detail (GET /api/workspace/members/:userId and /api/me self-view).
165
+ * Inferred from {@link WorkspaceMemberDetailSchema}.
166
+ */
167
+ export type WorkspaceMemberDetail = z.infer<typeof WorkspaceMemberDetailSchema>;
168
+
169
+ export const RoleCatalogEntrySchema = z.object({
170
+ name: z.string(),
171
+ type: z.enum(["system", "custom"]),
172
+ description: z.string(),
173
+ scopes: z.array(z.string()),
174
+ memberCount: z.number().int().nonnegative(),
175
+ });
176
+
177
+ export const RoleCatalogResponseSchema = z.object({
178
+ roles: z.array(RoleCatalogEntrySchema),
179
+ });
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // GET /api/workspace/members
183
+ // ---------------------------------------------------------------------------
184
+
185
+ export const WorkspaceMembersResponseSchema = CursorPageSchema(
186
+ WorkspaceMemberSchema,
187
+ );
188
+
189
+ export type WorkspaceMembersResponse = z.infer<
190
+ typeof WorkspaceMembersResponseSchema
191
+ >;
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // DELETE /api/workspace/members/:id
195
+ // ---------------------------------------------------------------------------
196
+
197
+ export const RemoveMemberResponseSchema = z.object({
198
+ success: z.boolean(),
199
+ memberId: z.string(),
200
+ memberEmail: z.string(),
201
+ message: z.string(),
202
+ });
203
+
204
+ export type RemoveMemberResponse = z.infer<typeof RemoveMemberResponseSchema>;
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // PATCH /api/workspace/members/:id/role
208
+ // ---------------------------------------------------------------------------
209
+
210
+ export const ChangeMemberRoleResponseSchema = z.object({
211
+ success: z.boolean(),
212
+ memberId: z.string(),
213
+ memberEmail: z.string(),
214
+ previousRole: z.string(),
215
+ newRole: z.string(),
216
+ message: z.string(),
217
+ });
218
+
219
+ export type ChangeMemberRoleResponse = z.infer<
220
+ typeof ChangeMemberRoleResponseSchema
221
+ >;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Workspace Overview Schemas
3
+ *
4
+ * Zod schemas for GET /api/workspace (the overview, its marks and owner icon)
5
+ * and GET /api/workspace/access. Re-exported from `./schemas`, the one import
6
+ * path for every org schema.
7
+ */
8
+ import { z } from "zod";
9
+
10
+ const WorkspaceOwnerSchema = z.object({
11
+ id: z.string(),
12
+ name: z.string(),
13
+ email: z.string(),
14
+ });
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // GET /api/workspace/access
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const WorkspaceAccessResponseSchema = z.object({
21
+ hasAccess: z.boolean(),
22
+ });
23
+
24
+ export type WorkspaceAccessResponse = z.infer<
25
+ typeof WorkspaceAccessResponseSchema
26
+ >;
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // GET /api/workspace
30
+ // PATCH /api/workspace/name (returns WorkspaceOverview)
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Icon options for the `owner` org-chart role (ADR-CONT-070). Selected
35
+ * alongside the owner title in the Organization structure editor; `crown` is
36
+ * the default. Presentation only — never an authorization input.
37
+ */
38
+ export const OrgOwnerIconSchema = z.enum([
39
+ "crown",
40
+ "crown-cross",
41
+ "person-simple-run",
42
+ "person",
43
+ "medal-military",
44
+ "dog",
45
+ "cat",
46
+ "steering-wheel",
47
+ ]);
48
+
49
+ /**
50
+ * An org's stored marks. See `OrgLogos` for what each slot means and
51
+ * `resolveOrgMark` for the order the square tile prefers them in.
52
+ */
53
+ export const OrgLogosSchema = z.object({
54
+ appLogoUrl: z.string().nullable(),
55
+ logoUrl: z.string().nullable(),
56
+ });
57
+
58
+ export type OrgLogosShape = z.infer<typeof OrgLogosSchema>;
59
+
60
+ export const WorkspaceOverviewSchema = z.object({
61
+ id: z.string(),
62
+ name: z.string(),
63
+ slug: z.string(),
64
+ type: z.enum(["personal", "shared"]),
65
+ /**
66
+ * The org's two marks. See `OrgLogos` for what each slot means and
67
+ * `resolveOrgMark` for the order the square tile prefers them in.
68
+ */
69
+ logos: OrgLogosSchema,
70
+ owner: WorkspaceOwnerSchema,
71
+ /**
72
+ * Org-configured display title for the `owner` org-chart role
73
+ * (ADR-CONTRACTS-061). Server-resolved: `orgs.owner_title` when set,
74
+ * otherwise the default "CEO". Always present; pass to
75
+ * `orgChartRoleLabel(role, { ownerTitle })` wherever the owner badge or
76
+ * section heading renders. Presentation only — never an authorization input.
77
+ */
78
+ ownerTitle: z.string(),
79
+ /**
80
+ * Org-configured icon for the `owner` org-chart role (ADR-CONT-070).
81
+ * Server-resolved: `orgs.owner_icon` when set, otherwise the default
82
+ * "crown". Always present. Presentation only — never an authorization input.
83
+ */
84
+ ownerIcon: OrgOwnerIconSchema,
85
+ createdAt: z.string(),
86
+ memberCount: z.number(),
87
+ claimable: z.boolean(),
88
+ });
89
+
90
+ export type OrgOwnerIcon = z.infer<typeof OrgOwnerIconSchema>;
91
+
92
+ export type WorkspaceOverview = z.infer<typeof WorkspaceOverviewSchema>;
@@ -18,6 +18,7 @@ guard's thresholds; `src/index.ts` re-exports every slice here with
18
18
  | `org-structure.ts` | sync runs, conflicts, reconciliation, structure findings, transformations, canonical facts, inference, authority |
19
19
  | `agent-runtime.ts` | MCP tool discovery, message parts, execution kinds, Ralph, internal admin, rate limits |
20
20
  | `platform.ts` | impersonation, usage telemetry, resource keys, errors, the generated OpenAPI surface, security, request tiers |
21
+ | `ci-governance.ts` | guard results and config, SOC 2 compliance, cross-repo CI aggregation, the compatibility manifest |
21
22
 
22
23
  ## Invariants
23
24
 
@@ -52,10 +53,13 @@ documented in its owning domain (`src/org/`, `src/chat/`, `src/comments/`, …).
52
53
  - `api`
53
54
  - `autotune`
54
55
  - `chat`
56
+ - `ci-results`
55
57
  - `comments`
58
+ - `compatibility`
56
59
  - `errors`
57
60
  - `execution`
58
61
  - `generated`
62
+ - `guards`
59
63
  - `impersonation`
60
64
  - `impersonation-events`
61
65
  - `internal-admin`
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Package root exports — CI governance
3
+ *
4
+ * Guard result and configuration vocabulary, SOC 2 compliance types, cross-repo
5
+ * CI aggregation, and the compatibility manifest.
6
+ *
7
+ * A slice of the `@company-semantics/contracts` root entry, moved here verbatim
8
+ * from `src/index.ts`, which re-exports it with `export *`. Each list is the
9
+ * exact subset of its domain the root exposes. Import from the package root,
10
+ * never from this file.
11
+ */
12
+
13
+ // Guard types (CI guard vocabulary)
14
+ // @see schemas/guard-result.schema.json for JSON Schema
15
+ export type {
16
+ GuardMessage,
17
+ GuardTierResult,
18
+ GuardTier,
19
+ GuardResults,
20
+ CheckResult,
21
+ GuardOutput,
22
+ RoleLimit,
23
+ GuardConfig,
24
+ GuardCheck,
25
+ GuardCheckFactory,
26
+ BoundGuardCheck,
27
+ GuardCheckRegistry,
28
+ StructuralBaselines,
29
+ ScriptsDepsBaseline,
30
+ EvolutionBaselines,
31
+ ContractsFreshnessBaseline,
32
+ MetaBaselines,
33
+ // Aggregate evolution types
34
+ DomainAggregateBaseline,
35
+ AggregateEvolutionConfig,
36
+ // SOC 2 Compliance types
37
+ Soc2ControlArea,
38
+ Soc2ControlStatus,
39
+ Soc2ControlResult,
40
+ Soc2ComplianceOutput,
41
+ // SOC 2 Guard Configuration types
42
+ SecretsDetectionConfig,
43
+ StructuredLoggingConfig,
44
+ AlertsConfigGuardConfig,
45
+ BackupConfigGuardConfig,
46
+ Soc2Baselines,
47
+ } from "../guards/index";
48
+
49
+ export {
50
+ GUARD_SCHEMA_VERSION,
51
+ GUARD_TIERS,
52
+ DEFAULT_SKIP_DIRECTORIES,
53
+ DEFAULT_DOMAIN_SECTIONS,
54
+ DEFAULT_INFRA_SECTIONS,
55
+ // SOC 2 Compliance constants
56
+ SOC2_CONTROL_NAMES,
57
+ REQUIRED_SOC2_CONTROLS,
58
+ BLOCKING_SOC2_CONTROLS,
59
+ SOC2_SCHEMA_VERSION,
60
+ } from "../guards/index";
61
+
62
+ // Cross-repo CI aggregation vocabulary
63
+ // @see ADR-CONT-033 for contracts boundary rationale
64
+ // Note: GuardTier is already exported above from './guards/index' (structurally
65
+ // identical); not re-exported here to avoid a duplicate-identifier collision.
66
+ export type { GuardResultPayload, RepoCiResult } from "../ci-results/index";
67
+
68
+ export { assertRepoCiResultConsistent } from "../ci-results/index";
69
+
70
+ // Compatibility manifest (CI guard vocabulary)
71
+ // @see ADR-2025-12-011 for design rationale
72
+ export { COMPATIBILITY } from "../compatibility";
73
+ export type { Compatibility, Deprecation } from "../compatibility";
@@ -92,9 +92,17 @@ way. This directory holds that shared spelling.
92
92
  A named person's pre-authorized instruction to keep one scope current, with
93
93
  an auto-apply ceiling of `none` or `low` — there is deliberately no higher
94
94
  member — a monthly spend cap in integer micro-USD and, when suspended, a
95
- closed reason. The request that sets one is strict and carries no grantor:
96
- the grantor is the authenticated caller, and the row's grantor label is
97
- display-only.
95
+ closed reason (`SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS`, including
96
+ `grantor_lost_doc_access`: the grantor kept the authority but lost upload and
97
+ edit access on the scope's document). The request that sets one is strict
98
+ and carries no grantor: the grantor is the authenticated caller, and the
99
+ row's grantor label is display-only. The list carries the caller's
100
+ grantability per scope (required); granting uses the same rule, and the same
101
+ blocked-reason vocabulary, as approving a scope.
102
+ - **One blocked-reason vocabulary.** Why the caller may not approve a scope or
103
+ grant its standing order: `needs_approval_authority` (no approval authority —
104
+ an org admin coordinates and never approves) or `needs_doc_access`. Authority
105
+ is checked first, so a caller lacking both reads `needs_approval_authority`.
98
106
  - **Drive reach names no credential and no person.** The reach status and the
99
107
  confirmed upgrade carry no key material, no delegated identity and no
100
108
  address; `hostedDomain` is a Workspace domain, and who changed reach is an
@@ -273,6 +281,9 @@ in `attention-evidence.ts`.
273
281
  `pending`, `approved` or `rejected` (no verb writes `rejected` yet).
274
282
  - `SOURCE_SYNC_SCOPE_APPROVER_KINDS` / `SourceSyncScopeApproverKind` —
275
283
  `org_admin` or `unit_head`.
284
+ - `SOURCE_SYNC_SCOPE_BLOCKED_REASONS` / `SourceSyncScopeBlockedReason` — why
285
+ the caller may not approve a scope or grant its standing order:
286
+ `needs_approval_authority` (checked first) or `needs_doc_access`.
276
287
  - `DOCUMENT_TYPES` / `DocumentType` / `KnownDocumentType` — what a source is,
277
288
  structurally; `KnownDocumentType` excludes `unknown` (see Document type).
278
289
  - `OPERATING_KNOWLEDGE_FAMILIES` / `OperatingKnowledgeFamily` — what kind of
@@ -369,8 +380,17 @@ in `attention-evidence.ts`.
369
380
  - `SourceSyncStandingOrderSchema` / `SourceSyncStandingOrder` — one standing
370
381
  order: scope, state, ceiling, monthly spend cap, grantor label, when granted
371
382
  and, if suspended, why.
383
+ - `SOURCE_SYNC_STANDING_ORDER_SUSPENDED_REASONS` /
384
+ `SourceSyncStandingOrderSuspendedReason` — why an order stopped acting:
385
+ `grantor_lost_authority`, `grantor_lost_doc_access`, `spend_cap_reached` or
386
+ `connection_lost`.
387
+ - `SourceSyncStandingOrderGrantabilitySchema` /
388
+ `SourceSyncStandingOrderGrantability` — whether the caller may grant one
389
+ scope's order; `blockedReason` (from `SOURCE_SYNC_SCOPE_BLOCKED_REASONS`) is
390
+ non-null exactly when it is not grantable. Advisory: the grant re-decides.
372
391
  - `SourceSyncStandingOrdersResponseSchema` /
373
- `SourceSyncStandingOrdersResponse` — the org's standing orders.
392
+ `SourceSyncStandingOrdersResponse` — the org's standing orders and the
393
+ caller's required `grantability`.
374
394
  - `SourceSyncPutStandingOrderRequestSchema` /
375
395
  `SourceSyncPutStandingOrderRequest` — set a scope's order: the ceiling and
376
396
  the spend cap, nothing else.
@@ -394,7 +414,8 @@ in `attention-evidence.ts`.
394
414
  - `../api/primitives` — the pinned `IsoDateTime`.
395
415
 
396
416
  `rules.ts` imports only `zod`, `../api/primitives` and `./vocabulary`.
397
- `upkeep.ts` and `reach.ts` import only `zod` and `../api/primitives`.
417
+ `upkeep.ts` imports only `zod`, `../api/primitives` and `./vocabulary`.
418
+ `reach.ts` imports only `zod` and `../api/primitives`.
398
419
  `candidate-item.ts` imports only `zod`, `../api/primitives`, `./vocabulary`,
399
420
  `./relevance` and `./attention-evidence`.
400
421
  `relevance.ts` imports only `zod` and `./enumeration`.
@@ -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";