@company-semantics/contracts 21.0.0 → 21.2.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.0.0",
3
+ "version": "21.2.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -82,7 +82,7 @@
82
82
  "build": "tsc -b --noEmit",
83
83
  "typecheck": "tsc -b --noEmit",
84
84
  "typecheck:ci": "tsc -p scripts/ci/tsconfig.json",
85
- "lint:md": "markdownlint-cli2 '**/*.md' '#node_modules'",
85
+ "lint:md": "markdownlint-cli2 '**/*.md' '#node_modules' '#.ralph/worktrees' '#.claude/worktrees'",
86
86
  "format": "prettier --write src/",
87
87
  "format:check": "prettier --check src/",
88
88
  "lint:json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\"",
@@ -30,6 +30,14 @@ export const HrConnectionStatusSchema = z
30
30
  connected: z.boolean().meta({
31
31
  description: "Whether an HR provider is currently connected.",
32
32
  }),
33
+ connectionId: z
34
+ .string()
35
+ .nullable()
36
+ .meta({
37
+ description:
38
+ "Stable id of the active connection — the handle the app passes to " +
39
+ "`integration.disconnect`. Null when not connected.",
40
+ }),
33
41
  employeeCount: z.number().int().nonnegative().nullable().meta({
34
42
  description:
35
43
  "Employees mirrored from the HR provider; null when never synced.",
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,
@@ -606,6 +606,46 @@ export type ScopeCheckBatchResponse = z.infer<
606
606
  export const HomeUnitRoleSchema = z.enum(["member", "unit_owner"]);
607
607
  export type HomeUnitRole = z.infer<typeof HomeUnitRoleSchema>;
608
608
 
609
+ /** Established title cap shared with org-unit/open-role titles. */
610
+ const STAGED_PLACEMENT_TITLE_MAX = 255;
611
+ /**
612
+ * Upper bound on the number of units an invitee can be staged into as owner or
613
+ * contributor. Bounds the replay cost of applying a staged placement at accept
614
+ * time (each entry is a deferred membership grant). A generous-but-finite cap.
615
+ */
616
+ const STAGED_PLACEMENT_UNITS_MAX = 64;
617
+
618
+ /**
619
+ * Staged member placement: the fuller org position an invitee is slotted into,
620
+ * applied as deferred grants when the invite is accepted. A superset of the
621
+ * legacy top-level `homeUnitId`/`homeUnitRole` fields, carrying the (optional)
622
+ * intended title, manager, and the units the invitee will own or contribute to.
623
+ *
624
+ * Every field is optional or defaulted so a placement can be partially
625
+ * specified at invite time. The array caps bound downstream replay cost.
626
+ */
627
+ export const StagedPlacementSchema = z.object({
628
+ // Intended job/role title for the invitee. Trimmed; non-empty when present.
629
+ title: z.string().trim().min(1).max(STAGED_PLACEMENT_TITLE_MAX).optional(),
630
+ // The invitee's manager on acceptance (a `manages` designation).
631
+ managerUserId: z.string().uuid().optional(),
632
+ // Home unit the invitee is placed in (their `users.primary_unit_id`).
633
+ homeUnitId: z.string().uuid().optional(),
634
+ // Role the invitee takes in `homeUnitId` ('unit_owner' = deferred grant).
635
+ homeUnitRole: HomeUnitRoleSchema.optional(),
636
+ // Additional units the invitee becomes a unit owner of on acceptance.
637
+ unitOwnerOf: z
638
+ .array(z.string().uuid())
639
+ .max(STAGED_PLACEMENT_UNITS_MAX)
640
+ .default([]),
641
+ // Units the invitee contributes to (non-owner membership) on acceptance.
642
+ contributesTo: z
643
+ .array(z.string().uuid())
644
+ .max(STAGED_PLACEMENT_UNITS_MAX)
645
+ .default([]),
646
+ });
647
+ export type StagedPlacement = z.infer<typeof StagedPlacementSchema>;
648
+
609
649
  const OrgInviteSchema = z.object({
610
650
  id: z.string(),
611
651
  orgId: z.string(),
@@ -620,6 +660,10 @@ const OrgInviteSchema = z.object({
620
660
  // deferred unit-ownership grant). Optional/'member' for legacy +
621
661
  // unit-less invites.
622
662
  homeUnitRole: HomeUnitRoleSchema.optional(),
663
+ // Fuller staged placement applied as deferred grants on acceptance. A
664
+ // superset of the top-level homeUnitId/homeUnitRole, kept additive/optional
665
+ // so legacy invites and unit-less invites parse unchanged.
666
+ stagedPlacement: StagedPlacementSchema.optional(),
623
667
  createdAt: z.string(),
624
668
  expiresAt: z.string(),
625
669
  acceptedAt: z.string().optional(),
@@ -643,6 +687,33 @@ export const InviteListResponseSchema = z.array(OrgInviteSchema);
643
687
 
644
688
  export type InviteListResponse = z.infer<typeof InviteListResponseSchema>;
645
689
 
690
+ // ---------------------------------------------------------------------------
691
+ // POST /api/workspace/invites — request body
692
+ // Request schema colocated with the response schema (narrow deviation from
693
+ // ADR-CONT-044, per the CreateOpenRoleRequestSchema precedent) because the
694
+ // staged-placement shape is the published promise consumers must satisfy.
695
+ // Mirrors the hand-written `CreateInviteRequest` interface in org/types.ts and
696
+ // extends it with the optional `stagedPlacement` superset.
697
+ // ---------------------------------------------------------------------------
698
+
699
+ export const CreateInviteRequestSchema = z.object({
700
+ email: z.string().email(),
701
+ // Invites only grant the restricted {admin, member} domain.
702
+ role: z.enum(["admin", "member"]),
703
+ // Home unit the invitee is placed in on acceptance. Optional in the contract
704
+ // so the field can roll out without breaking older callers.
705
+ homeUnitId: z.string().uuid().optional(),
706
+ // Role the invitee takes in homeUnitId — only meaningful with a home unit.
707
+ homeUnitRole: HomeUnitRoleSchema.optional(),
708
+ // Fuller staged placement (title, manager, owned/contributed units) applied
709
+ // as deferred grants on acceptance. Optional/additive for back-compat.
710
+ stagedPlacement: StagedPlacementSchema.optional(),
711
+ });
712
+
713
+ export type CreateInviteRequestPayload = z.infer<
714
+ typeof CreateInviteRequestSchema
715
+ >;
716
+
646
717
  // ---------------------------------------------------------------------------
647
718
  // OrgDomain sub-schema
648
719
  // ---------------------------------------------------------------------------
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
  /**