@company-semantics/contracts 21.1.0 → 21.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "21.1.0",
3
+ "version": "21.3.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '04f6a4a8fe16' as const;
3
- export const SPEC_HASH_FULL = '04f6a4a8fe169119fda699f1854d409df278ac5dd7ab16f1d5affff6cd133e47' as const;
2
+ export const SPEC_HASH = '16094a4fcda0' as const;
3
+ export const SPEC_HASH_FULL = '16094a4fcda0647e96289cdc0e104eab13d9539c043207cdda1ad8c8bf5a4dcb' as const;
@@ -3449,6 +3449,8 @@ export interface components {
3449
3449
  unitName: string;
3450
3450
  /** @enum {string} */
3451
3451
  designation: "whole_org" | "admin" | "unit_owner" | "delegate" | "member";
3452
+ /** @enum {string} */
3453
+ mutability: "derived" | "user_managed";
3452
3454
  }[];
3453
3455
  unitMembershipsTruncated: boolean;
3454
3456
  inviteStatus: ("active" | "pending" | "expired") | null;
@@ -3644,6 +3646,8 @@ export interface components {
3644
3646
  unitName: string;
3645
3647
  /** @enum {string} */
3646
3648
  designation: "whole_org" | "admin" | "unit_owner" | "delegate" | "member";
3649
+ /** @enum {string} */
3650
+ mutability: "derived" | "user_managed";
3647
3651
  }[];
3648
3652
  unitMembershipsTruncated: boolean;
3649
3653
  inviteStatus: ("active" | "pending" | "expired") | null;
@@ -3774,6 +3778,19 @@ export interface components {
3774
3778
  homeUnitId?: string;
3775
3779
  /** @enum {string} */
3776
3780
  homeUnitRole?: "member" | "unit_owner";
3781
+ stagedPlacement?: {
3782
+ title?: string;
3783
+ /** Format: uuid */
3784
+ managerUserId?: string;
3785
+ /** Format: uuid */
3786
+ homeUnitId?: string;
3787
+ /** @enum {string} */
3788
+ homeUnitRole?: "member" | "unit_owner";
3789
+ /** @default [] */
3790
+ unitOwnerOf: string[];
3791
+ /** @default [] */
3792
+ contributesTo: string[];
3793
+ };
3777
3794
  createdAt: string;
3778
3795
  expiresAt: string;
3779
3796
  acceptedAt?: string;
@@ -3805,6 +3822,19 @@ export interface components {
3805
3822
  homeUnitId?: string;
3806
3823
  /** @enum {string} */
3807
3824
  homeUnitRole?: "member" | "unit_owner";
3825
+ stagedPlacement?: {
3826
+ title?: string;
3827
+ /** Format: uuid */
3828
+ managerUserId?: string;
3829
+ /** Format: uuid */
3830
+ homeUnitId?: string;
3831
+ /** @enum {string} */
3832
+ homeUnitRole?: "member" | "unit_owner";
3833
+ /** @default [] */
3834
+ unitOwnerOf: string[];
3835
+ /** @default [] */
3836
+ contributesTo: string[];
3837
+ };
3808
3838
  createdAt: string;
3809
3839
  expiresAt: string;
3810
3840
  acceptedAt?: string;
package/src/org/README.md CHANGED
@@ -75,6 +75,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
75
75
  - `CreateDelegationRequest` _(type)_
76
76
  - `CreateDelegationRequestSchema`
77
77
  - `CreateInviteRequest` _(type)_ — Request payload for creating an organization invite.
78
+ - `CreateInviteRequestPayload` _(type)_
79
+ - `CreateInviteRequestSchema`
78
80
  - `CreateOpenRoleRequest` _(type)_
79
81
  - `CreateOpenRoleRequestSchema`
80
82
  - `Delegation` _(type)_
@@ -258,6 +260,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
258
260
  - `SsoReadinessCheck` _(type)_ — Individual readiness check for SSO activation.
259
261
  - `SsoSetupInfo` _(type)_ — SSO setup information for admin configuration UI.
260
262
  - `SsoStepperStep` _(type)_ — Backend-authoritative stepper step.
263
+ - `StagedPlacement` _(type)_ — Staged member placement: the fuller org position an invitee is slotted into, applied as deferred grants when…
264
+ - `StagedPlacementSchema` — Staged member placement: the fuller org position an invitee is slotted into, applied as deferred grants when…
261
265
  - `SubmitInteractiveTaskResponse` _(type)_
262
266
  - `SubmitInteractiveTaskResponseSchema` — Response for submitting a filled-in interactive chat task surface (e.g. the editable "change reporting…
263
267
  - `SyncRunSummary` _(type)_
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ StagedPlacementSchema,
4
+ CreateInviteRequestSchema,
5
+ InviteResponseSchema,
6
+ } from "../schemas.js";
7
+
8
+ const UUID_A = "11111111-1111-4111-8111-111111111111";
9
+ const UUID_B = "22222222-2222-4222-8222-222222222222";
10
+ const UUID_C = "33333333-3333-4333-8333-333333333333";
11
+
12
+ const makeInvite = (overrides: Record<string, unknown> = {}) => ({
13
+ id: UUID_A,
14
+ orgId: UUID_B,
15
+ email: "new.hire@example.com",
16
+ role: "member",
17
+ invitedBy: { id: UUID_C, name: "Inviter" },
18
+ status: "pending",
19
+ createdAt: "2026-06-24T00:00:00Z",
20
+ expiresAt: "2026-07-01T00:00:00Z",
21
+ ...overrides,
22
+ });
23
+
24
+ describe("CreateInviteRequestSchema", () => {
25
+ it("parses a minimal payload without stagedPlacement", () => {
26
+ const parsed = CreateInviteRequestSchema.parse({
27
+ email: "new.hire@example.com",
28
+ role: "admin",
29
+ });
30
+ expect(parsed.stagedPlacement).toBeUndefined();
31
+ expect(parsed.homeUnitId).toBeUndefined();
32
+ });
33
+
34
+ it("parses a full stagedPlacement with arrays defaulting to []", () => {
35
+ const parsed = CreateInviteRequestSchema.parse({
36
+ email: "new.hire@example.com",
37
+ role: "member",
38
+ homeUnitId: UUID_A,
39
+ homeUnitRole: "unit_owner",
40
+ stagedPlacement: {
41
+ title: "Staff Engineer",
42
+ managerUserId: UUID_B,
43
+ homeUnitId: UUID_A,
44
+ homeUnitRole: "unit_owner",
45
+ },
46
+ });
47
+ expect(parsed.stagedPlacement?.title).toBe("Staff Engineer");
48
+ // unitOwnerOf/contributesTo default to [] when omitted.
49
+ expect(parsed.stagedPlacement?.unitOwnerOf).toEqual([]);
50
+ expect(parsed.stagedPlacement?.contributesTo).toEqual([]);
51
+ });
52
+ });
53
+
54
+ describe("StagedPlacementSchema", () => {
55
+ it("defaults unitOwnerOf and contributesTo to []", () => {
56
+ const parsed = StagedPlacementSchema.parse({});
57
+ expect(parsed.unitOwnerOf).toEqual([]);
58
+ expect(parsed.contributesTo).toEqual([]);
59
+ });
60
+
61
+ it("retains provided unit arrays", () => {
62
+ const parsed = StagedPlacementSchema.parse({
63
+ unitOwnerOf: [UUID_A],
64
+ contributesTo: [UUID_B, UUID_C],
65
+ });
66
+ expect(parsed.unitOwnerOf).toEqual([UUID_A]);
67
+ expect(parsed.contributesTo).toEqual([UUID_B, UUID_C]);
68
+ });
69
+
70
+ it("trims the title", () => {
71
+ const parsed = StagedPlacementSchema.parse({ title: " Lead " });
72
+ expect(parsed.title).toBe("Lead");
73
+ });
74
+
75
+ it("rejects a non-uuid managerUserId", () => {
76
+ expect(() =>
77
+ StagedPlacementSchema.parse({ managerUserId: "not-a-uuid" }),
78
+ ).toThrow();
79
+ });
80
+ });
81
+
82
+ describe("OrgInviteSchema (via InviteResponseSchema)", () => {
83
+ it("parses an invite without stagedPlacement (back-compat)", () => {
84
+ const parsed = InviteResponseSchema.parse({ invite: makeInvite() });
85
+ expect(parsed.invite.stagedPlacement).toBeUndefined();
86
+ });
87
+
88
+ it("parses an invite carrying a stagedPlacement", () => {
89
+ const parsed = InviteResponseSchema.parse({
90
+ invite: makeInvite({
91
+ homeUnitId: UUID_A,
92
+ homeUnitRole: "member",
93
+ stagedPlacement: {
94
+ title: "Engineer",
95
+ unitOwnerOf: [UUID_A],
96
+ },
97
+ }),
98
+ });
99
+ expect(parsed.invite.stagedPlacement?.title).toBe("Engineer");
100
+ expect(parsed.invite.stagedPlacement?.unitOwnerOf).toEqual([UUID_A]);
101
+ // Omitted contributesTo still defaults to [].
102
+ expect(parsed.invite.stagedPlacement?.contributesTo).toEqual([]);
103
+ });
104
+ });
package/src/org/index.ts CHANGED
@@ -249,6 +249,8 @@ export type {
249
249
 
250
250
  // Org lifecycle response schemas (PRD-00446)
251
251
  export {
252
+ StagedPlacementSchema,
253
+ CreateInviteRequestSchema,
252
254
  InviteResponseSchema,
253
255
  InviteListResponseSchema,
254
256
  DomainResponseSchema,
@@ -259,6 +261,8 @@ export {
259
261
  OwnershipTransferPreviewSchema,
260
262
  } from "./schemas";
261
263
  export type {
264
+ StagedPlacement,
265
+ CreateInviteRequestPayload,
262
266
  InviteResponse,
263
267
  InviteListResponse,
264
268
  DomainResponse,
@@ -43,10 +43,31 @@ export const OrgUnitDesignationSchema = z.enum([
43
43
  ]);
44
44
  export type OrgUnitDesignation = z.infer<typeof OrgUnitDesignationSchema>;
45
45
 
46
+ /**
47
+ * Whether the user can change this authority from this surface.
48
+ * - `derived`: change the upstream source instead (the org chart, RBAC roles).
49
+ * - `user_managed`: removable from this UI.
50
+ *
51
+ * Hoisted above {@link WorkspaceMemberUnitDesignationEntrySchema} so the
52
+ * per-unit designation can carry it; also consumed by {@link OwnerAuthoritySchema}.
53
+ */
54
+ export const AuthorityMutabilitySchema = z.enum(["derived", "user_managed"]);
55
+ export type AuthorityMutability = z.infer<typeof AuthorityMutabilitySchema>;
56
+
46
57
  export const WorkspaceMemberUnitDesignationEntrySchema = z.object({
47
58
  unitId: z.string(),
48
59
  unitName: z.string(),
49
60
  designation: OrgUnitDesignationSchema,
61
+ /**
62
+ * Whether this designation is REMOVABLE in place (`user_managed` — an explicit
63
+ * overlay grant or `l{N}_unit_owner` membership) or DERIVED from reporting
64
+ * topology (`derived` — the senior person homed in a unit with no manager
65
+ * inside it, ADR-BE-267/294). A `derived` owner can't be revoked: the holder
66
+ * must relinquish it (recorded as a higher-provenance suppression that the
67
+ * structural projection honors) or the upstream chart must change. Display +
68
+ * affordance gating only; never an authorization input.
69
+ */
70
+ mutability: AuthorityMutabilitySchema,
50
71
  });
51
72
  export type WorkspaceMemberUnitDesignationEntry = z.infer<
52
73
  typeof WorkspaceMemberUnitDesignationEntrySchema
@@ -606,6 +627,46 @@ export type ScopeCheckBatchResponse = z.infer<
606
627
  export const HomeUnitRoleSchema = z.enum(["member", "unit_owner"]);
607
628
  export type HomeUnitRole = z.infer<typeof HomeUnitRoleSchema>;
608
629
 
630
+ /** Established title cap shared with org-unit/open-role titles. */
631
+ const STAGED_PLACEMENT_TITLE_MAX = 255;
632
+ /**
633
+ * Upper bound on the number of units an invitee can be staged into as owner or
634
+ * contributor. Bounds the replay cost of applying a staged placement at accept
635
+ * time (each entry is a deferred membership grant). A generous-but-finite cap.
636
+ */
637
+ const STAGED_PLACEMENT_UNITS_MAX = 64;
638
+
639
+ /**
640
+ * Staged member placement: the fuller org position an invitee is slotted into,
641
+ * applied as deferred grants when the invite is accepted. A superset of the
642
+ * legacy top-level `homeUnitId`/`homeUnitRole` fields, carrying the (optional)
643
+ * intended title, manager, and the units the invitee will own or contribute to.
644
+ *
645
+ * Every field is optional or defaulted so a placement can be partially
646
+ * specified at invite time. The array caps bound downstream replay cost.
647
+ */
648
+ export const StagedPlacementSchema = z.object({
649
+ // Intended job/role title for the invitee. Trimmed; non-empty when present.
650
+ title: z.string().trim().min(1).max(STAGED_PLACEMENT_TITLE_MAX).optional(),
651
+ // The invitee's manager on acceptance (a `manages` designation).
652
+ managerUserId: z.string().uuid().optional(),
653
+ // Home unit the invitee is placed in (their `users.primary_unit_id`).
654
+ homeUnitId: z.string().uuid().optional(),
655
+ // Role the invitee takes in `homeUnitId` ('unit_owner' = deferred grant).
656
+ homeUnitRole: HomeUnitRoleSchema.optional(),
657
+ // Additional units the invitee becomes a unit owner of on acceptance.
658
+ unitOwnerOf: z
659
+ .array(z.string().uuid())
660
+ .max(STAGED_PLACEMENT_UNITS_MAX)
661
+ .default([]),
662
+ // Units the invitee contributes to (non-owner membership) on acceptance.
663
+ contributesTo: z
664
+ .array(z.string().uuid())
665
+ .max(STAGED_PLACEMENT_UNITS_MAX)
666
+ .default([]),
667
+ });
668
+ export type StagedPlacement = z.infer<typeof StagedPlacementSchema>;
669
+
609
670
  const OrgInviteSchema = z.object({
610
671
  id: z.string(),
611
672
  orgId: z.string(),
@@ -620,6 +681,10 @@ const OrgInviteSchema = z.object({
620
681
  // deferred unit-ownership grant). Optional/'member' for legacy +
621
682
  // unit-less invites.
622
683
  homeUnitRole: HomeUnitRoleSchema.optional(),
684
+ // Fuller staged placement applied as deferred grants on acceptance. A
685
+ // superset of the top-level homeUnitId/homeUnitRole, kept additive/optional
686
+ // so legacy invites and unit-less invites parse unchanged.
687
+ stagedPlacement: StagedPlacementSchema.optional(),
623
688
  createdAt: z.string(),
624
689
  expiresAt: z.string(),
625
690
  acceptedAt: z.string().optional(),
@@ -643,6 +708,33 @@ export const InviteListResponseSchema = z.array(OrgInviteSchema);
643
708
 
644
709
  export type InviteListResponse = z.infer<typeof InviteListResponseSchema>;
645
710
 
711
+ // ---------------------------------------------------------------------------
712
+ // POST /api/workspace/invites — request body
713
+ // Request schema colocated with the response schema (narrow deviation from
714
+ // ADR-CONT-044, per the CreateOpenRoleRequestSchema precedent) because the
715
+ // staged-placement shape is the published promise consumers must satisfy.
716
+ // Mirrors the hand-written `CreateInviteRequest` interface in org/types.ts and
717
+ // extends it with the optional `stagedPlacement` superset.
718
+ // ---------------------------------------------------------------------------
719
+
720
+ export const CreateInviteRequestSchema = z.object({
721
+ email: z.string().email(),
722
+ // Invites only grant the restricted {admin, member} domain.
723
+ role: z.enum(["admin", "member"]),
724
+ // Home unit the invitee is placed in on acceptance. Optional in the contract
725
+ // so the field can roll out without breaking older callers.
726
+ homeUnitId: z.string().uuid().optional(),
727
+ // Role the invitee takes in homeUnitId — only meaningful with a home unit.
728
+ homeUnitRole: HomeUnitRoleSchema.optional(),
729
+ // Fuller staged placement (title, manager, owned/contributed units) applied
730
+ // as deferred grants on acceptance. Optional/additive for back-compat.
731
+ stagedPlacement: StagedPlacementSchema.optional(),
732
+ });
733
+
734
+ export type CreateInviteRequestPayload = z.infer<
735
+ typeof CreateInviteRequestSchema
736
+ >;
737
+
646
738
  // ---------------------------------------------------------------------------
647
739
  // OrgDomain sub-schema
648
740
  // ---------------------------------------------------------------------------
@@ -1070,13 +1162,8 @@ export type AuthorityMechanism = z.infer<typeof AuthorityMechanismSchema>;
1070
1162
  export const AuthorityLocalitySchema = z.enum(["local", "inherited"]);
1071
1163
  export type AuthorityLocality = z.infer<typeof AuthorityLocalitySchema>;
1072
1164
 
1073
- /**
1074
- * Whether the user can change this authority from this surface.
1075
- * - `derived`: change the upstream source instead (the org chart, RBAC roles).
1076
- * - `user_managed`: removable from this UI.
1077
- */
1078
- export const AuthorityMutabilitySchema = z.enum(["derived", "user_managed"]);
1079
- export type AuthorityMutability = z.infer<typeof AuthorityMutabilitySchema>;
1165
+ // AuthorityMutabilitySchema is defined above (hoisted next to
1166
+ // WorkspaceMemberUnitDesignationEntrySchema, which also carries it).
1080
1167
 
1081
1168
  export const OwnerAuthoritySchema = z.object({
1082
1169
  mechanism: AuthorityMechanismSchema,
package/src/org/types.ts CHANGED
@@ -397,6 +397,37 @@ export interface WorkspaceAuditEvent {
397
397
  */
398
398
  export type OrgInviteStatus = "pending" | "accepted" | "expired" | "revoked";
399
399
 
400
+ /**
401
+ * Staged member placement: the fuller org position an invitee is slotted into,
402
+ * applied as deferred grants when the invite is accepted. A superset of the
403
+ * top-level `homeUnitId`/`homeUnitRole` fields, carrying the (optional) intended
404
+ * title, manager, and the units the invitee will own or contribute to.
405
+ *
406
+ * Hand-written mirror of `StagedPlacementSchema` in `org/schemas.ts` — keep in
407
+ * lockstep. Because the schema defaults `unitOwnerOf`/`contributesTo` to `[]`,
408
+ * their inferred OUTPUT type is a required `string[]`; the other four fields are
409
+ * optional. The `homeUnitRole` union is re-typed inline (not imported) to match
410
+ * the `OrgInvite`/`CreateInviteRequest` convention.
411
+ *
412
+ * NOTE: the canonical `StagedPlacement` name is exported package-root from
413
+ * `org/schemas.ts` (the `z.infer`). This interface is intentionally NOT added to
414
+ * the `./types` re-export block in `org/index.ts` to avoid a name collision.
415
+ */
416
+ export interface StagedPlacement {
417
+ /** Intended job/role title for the invitee. Trimmed; non-empty when present. */
418
+ title?: string;
419
+ /** The invitee's manager on acceptance (a `manages` designation) — a member id. */
420
+ managerUserId?: string;
421
+ /** Home unit the invitee is placed in (their `users.primary_unit_id`). */
422
+ homeUnitId?: string;
423
+ /** Role the invitee takes in `homeUnitId` (`unit_owner` = deferred grant). */
424
+ homeUnitRole?: "member" | "unit_owner";
425
+ /** Additional units the invitee becomes a unit owner of on acceptance. */
426
+ unitOwnerOf: string[];
427
+ /** Units the invitee contributes to (non-owner membership) on acceptance. */
428
+ contributesTo: string[];
429
+ }
430
+
400
431
  /**
401
432
  * Organization invite for the workspace invites list.
402
433
  * Represents a pending or historical invitation.
@@ -425,6 +456,13 @@ export interface OrgInvite {
425
456
  * legacy invites and unit-less invites have none (treated as `member`).
426
457
  */
427
458
  homeUnitRole?: "member" | "unit_owner";
459
+ /**
460
+ * Fuller staged placement applied as deferred grants on acceptance. A superset
461
+ * of the top-level `homeUnitId`/`homeUnitRole`, kept additive/optional so
462
+ * legacy and unit-less invites parse unchanged. Mirrors
463
+ * `OrgInviteSchema.stagedPlacement` in `org/schemas.ts`.
464
+ */
465
+ stagedPlacement?: StagedPlacement;
428
466
  createdAt: string;
429
467
  expiresAt: string;
430
468
  acceptedAt?: string;
@@ -450,6 +488,12 @@ export interface CreateInviteRequest {
450
488
  * rejects `unit_owner` without a home unit. Optional/`member` for older callers.
451
489
  */
452
490
  homeUnitRole?: "member" | "unit_owner";
491
+ /**
492
+ * Fuller staged placement (title, manager, owned/contributed units) applied as
493
+ * deferred grants on acceptance. Optional/additive for back-compat. Mirrors
494
+ * `CreateInviteRequestSchema.stagedPlacement` in `org/schemas.ts`.
495
+ */
496
+ stagedPlacement?: StagedPlacement;
453
497
  }
454
498
 
455
499
  /**