@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.
@@ -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";
@@ -23,6 +23,8 @@ ledger and reconciles) can never disagree about a slot.
23
23
  `contentHash` is computed over.
24
24
  - Response schemas only; request bodies live in the backend beside their routes
25
25
  (ScheduleTickRequest is a type, not a schema).
26
+ - A re-prove grants exactly one extra attempt for a named execution; the route
27
+ only grants, the backend clock dispatches.
26
28
  - Pure: no clock reads, no I/O, only `zod`.
27
29
 
28
30
  ## Dependencies
@@ -64,6 +66,9 @@ ledger and reconciles) can never disagree about a slot.
64
66
  - `SCHEDULE_MODES` — off: GitHub cron only, clock ignores it. shadow: clock plans, never dispatches. live: clock dispatches…
65
67
  - `SCHEDULE_OUTPUT_VERIFIERS` — Closed catalog. v0 has one verifier.
66
68
  - `SCHEDULE_REGISTRY_SCHEMA_VERSION`
69
+ - `SCHEDULE_REPROVE_PATH` — The operator re-prove route (control ADR slug operator-schedule-reprove).
70
+ - `SCHEDULE_REPROVE_REASON_MAX_LENGTH` — Upper bound of a re-prove reason (operator prose, kept for audit only).
71
+ - `SCHEDULE_REPROVE_REFUSALS` — Why a re-prove was refused.
67
72
  - `SCHEDULE_RUN_CONCLUSIONS`
68
73
  - `SCHEDULE_RUN_NAME` — The exact `run-name:` a registered workflow declares, so displaytitle = executionid#attempt.
69
74
  - `SCHEDULE_SEVERITIES`
@@ -91,6 +96,10 @@ ledger and reconciles) can never disagree about a slot.
91
96
  - `ScheduleOutputVerifier` _(type)_
92
97
  - `ScheduleRegistrySnapshot` _(type)_
93
98
  - `ScheduleRegistrySnapshotSchema`
99
+ - `ScheduleReproveRefusal` _(type)_
100
+ - `ScheduleReproveRequest` _(type)_ — Re-prove request body, as a TYPE only: the backend binds its zod schema to it with `satisfies…
101
+ - `ScheduleReproveResponse` _(type)_
102
+ - `ScheduleReproveResponseSchema` — `granted`: the clock will dispatch `<executionId>#<grantedAttempt>` on its next pass.
94
103
  - `ScheduleRunConclusion` _(type)_
95
104
  - `ScheduleSeverity` _(type)_
96
105
  - `ScheduleTickRequest` _(type)_ — Tick request body, as a TYPE only: request schemas stay in the backend beside the route (contracts…
@@ -5,9 +5,12 @@ import {
5
5
  SCHEDULE_METRIC_TICK,
6
6
  SCHEDULE_METRIC_TICK_SOURCE_DIMENSION,
7
7
  SCHEDULE_METRIC_ENVIRONMENT_DIMENSION,
8
+ SCHEDULE_REPROVE_PATH,
9
+ SCHEDULE_REPROVE_REFUSALS,
8
10
  SCHEDULE_TICK_PATH,
9
11
  ScheduleClaimResponseSchema,
10
12
  ScheduleFinishResponseSchema,
13
+ ScheduleReproveResponseSchema,
11
14
  ScheduleTickResponseSchema,
12
15
  } from "../api";
13
16
 
@@ -69,4 +72,52 @@ describe("schedules api responses", () => {
69
72
  "environment",
70
73
  ]);
71
74
  });
75
+
76
+ it("grants a re-prove for a named attempt", () => {
77
+ expect(SCHEDULE_REPROVE_PATH).toBe("/api/schedules/executions/reprove");
78
+ expect(
79
+ ScheduleReproveResponseSchema.safeParse({
80
+ outcome: "granted",
81
+ executionId: "control-pin-audit/2026-10-12T15:00:00Z",
82
+ grantedAttempt: 4,
83
+ incidentKey:
84
+ "schedule:execution:control-pin-audit/2026-10-12T15:00:00Z",
85
+ incidentNoted: true,
86
+ }).success,
87
+ ).toBe(true);
88
+ expect(
89
+ ScheduleReproveResponseSchema.safeParse({
90
+ outcome: "granted",
91
+ executionId: "control-pin-audit/2026-10-12T15:00:00Z",
92
+ grantedAttempt: 1,
93
+ incidentKey: null,
94
+ incidentNoted: false,
95
+ }).success,
96
+ ).toBe(false);
97
+ });
98
+
99
+ it("refuses a re-prove with a closed reason", () => {
100
+ expect(SCHEDULE_REPROVE_REFUSALS).toEqual([
101
+ "unknown_execution",
102
+ "schedule_not_live",
103
+ "not_exhausted",
104
+ "not_reprovable",
105
+ "schedule_busy",
106
+ "grant_lost",
107
+ ]);
108
+ expect(
109
+ ScheduleReproveResponseSchema.safeParse({
110
+ outcome: "refused",
111
+ executionId: "x/2026-10-12T15:00:00Z",
112
+ refusal: "not_exhausted",
113
+ }).success,
114
+ ).toBe(true);
115
+ expect(
116
+ ScheduleReproveResponseSchema.safeParse({
117
+ outcome: "refused",
118
+ executionId: "x/2026-10-12T15:00:00Z",
119
+ refusal: "because",
120
+ }).success,
121
+ ).toBe(false);
122
+ });
72
123
  });
@@ -19,6 +19,9 @@ describe("schedules exports", () => {
19
19
  "ScheduleClaimResponseSchema",
20
20
  "SCHEDULE_TICK_PATH",
21
21
  "SCHEDULE_METRICS_NAMESPACE",
22
+ "SCHEDULE_REPROVE_PATH",
23
+ "SCHEDULE_REPROVE_REFUSALS",
24
+ "ScheduleReproveResponseSchema",
22
25
  ]) {
23
26
  expect(root).toHaveProperty(name);
24
27
  }
@@ -137,3 +137,76 @@ export const ScheduleTickResponseSchema = z
137
137
  })
138
138
  .strict();
139
139
  export type ScheduleTickResponse = z.infer<typeof ScheduleTickResponseSchema>;
140
+
141
+ /**
142
+ * The operator re-prove route (control ADR slug operator-schedule-reprove).
143
+ * Its only caller is control's schedules-reprove.yml on main, authenticated by
144
+ * that job's GitHub OIDC token; an operator starts the workflow with
145
+ * `pnpm schedules:reprove <scheduleId> <slot> --reason "…"`. The route only
146
+ * GRANTS one extra attempt for a named execution; the backend clock dispatches
147
+ * it through the normal claim-before-call path.
148
+ */
149
+ export const SCHEDULE_REPROVE_PATH = "/api/schedules/executions/reprove";
150
+
151
+ /** Upper bound of a re-prove reason (operator prose, kept for audit only). */
152
+ export const SCHEDULE_REPROVE_REASON_MAX_LENGTH = 500;
153
+
154
+ /**
155
+ * Re-prove request body, as a TYPE only: the backend binds its zod schema to it
156
+ * with `satisfies z.ZodType<ScheduleReproveRequest>`. `executionId` names the
157
+ * exhausted slot (`<schedule_id>/<slot>`); `reason` (1..500 chars) says why the
158
+ * operator believes the cause is fixed.
159
+ */
160
+ export interface ScheduleReproveRequest {
161
+ readonly executionId: string;
162
+ readonly reason: string;
163
+ }
164
+
165
+ /**
166
+ * Why a re-prove was refused. Closed.
167
+ * - unknown_execution: no ledger row for that execution id.
168
+ * - schedule_not_live: not in the active registry, or its mode is not live.
169
+ * - not_exhausted: the clock still owns the slot (attempts remain and effects
170
+ * allow re-execution, a grant is unspent, or admission has not timed out).
171
+ * - not_reprovable: succeeded, settled, due, started, or failed only its
172
+ * output verification.
173
+ * - schedule_busy: another execution of the same schedule is in flight.
174
+ * - grant_lost: a concurrent grant or dispatch changed the row first.
175
+ */
176
+ export const SCHEDULE_REPROVE_REFUSALS = [
177
+ "unknown_execution",
178
+ "schedule_not_live",
179
+ "not_exhausted",
180
+ "not_reprovable",
181
+ "schedule_busy",
182
+ "grant_lost",
183
+ ] as const;
184
+ export type ScheduleReproveRefusal = (typeof SCHEDULE_REPROVE_REFUSALS)[number];
185
+
186
+ /**
187
+ * `granted`: the clock will dispatch `<executionId>#<grantedAttempt>` on its
188
+ * next pass. `incidentKey` is the open execution/admission incident naming the
189
+ * slot (null when none); `incidentNoted` says whether the grant was commented
190
+ * on its issue (a failed comment never revokes the grant).
191
+ */
192
+ export const ScheduleReproveResponseSchema = z.discriminatedUnion("outcome", [
193
+ z
194
+ .object({
195
+ outcome: z.literal("granted"),
196
+ executionId: z.string(),
197
+ grantedAttempt: z.number().int().min(2),
198
+ incidentKey: z.string().nullable(),
199
+ incidentNoted: z.boolean(),
200
+ })
201
+ .strict(),
202
+ z
203
+ .object({
204
+ outcome: z.literal("refused"),
205
+ executionId: z.string(),
206
+ refusal: z.enum(SCHEDULE_REPROVE_REFUSALS),
207
+ })
208
+ .strict(),
209
+ ]);
210
+ export type ScheduleReproveResponse = z.infer<
211
+ typeof ScheduleReproveResponseSchema
212
+ >;
@@ -108,6 +108,10 @@ export {
108
108
  ScheduleFinishResponseSchema,
109
109
  ScheduleTickResponseSchema,
110
110
  SCHEDULE_TICK_PATH,
111
+ SCHEDULE_REPROVE_PATH,
112
+ SCHEDULE_REPROVE_REASON_MAX_LENGTH,
113
+ SCHEDULE_REPROVE_REFUSALS,
114
+ ScheduleReproveResponseSchema,
111
115
  SCHEDULE_METRICS_NAMESPACE,
112
116
  SCHEDULE_METRIC_TICK,
113
117
  SCHEDULE_METRIC_TICK_SOURCE_DIMENSION,
@@ -121,4 +125,7 @@ export type {
121
125
  ScheduleFinishResponse,
122
126
  ScheduleTickResponse,
123
127
  ScheduleTickRequest,
128
+ ScheduleReproveRequest,
129
+ ScheduleReproveRefusal,
130
+ ScheduleReproveResponse,
124
131
  } from "./api";
@@ -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`.