@typeb-digital/nucleus-sdk 0.2.1 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,114 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the
7
7
  package is pre-`1.0.0`, **breaking changes may land in a minor version** (e.g. `0.0.x` → `0.1.0`).
8
8
 
9
+ ## [0.3.1] - 2026-07-07
10
+
11
+ Calendar-event jurisdiction support. Additive and non-breaking.
12
+
13
+ ### Added
14
+
15
+ - **`calendarEvents` expose jurisdiction.** `CalendarEventCore` now includes
16
+ `jurisdictionId: string | null` (which jurisdiction a holiday applies to; null for
17
+ non-holiday events) and `national: boolean` (national vs region-specific holiday).
18
+ - **Expandable `jurisdiction`.** `calendarEvents.list(...)` and `calendarEvents.getById(...)`
19
+ accept `expand: ['jurisdiction']` to hydrate the full jurisdiction inline
20
+ (`event.jurisdiction` → the resolved jurisdiction, or `null`). Requires a `jurisdictions`
21
+ read scope on the token (the platform enforces scope on expanded resources). Example:
22
+
23
+ ```ts
24
+ const { data } = await nucleus.calendarEvents.list({
25
+ type: 'holiday',
26
+ expand: ['jurisdiction'],
27
+ });
28
+ data[0].jurisdiction?.name; // typed only when expanded
29
+ ```
30
+
31
+ ## [0.3.0] - 2026-07-01
32
+
33
+ Leveling model v2 and app-writable employee data. **This is a breaking release** — the
34
+ `employees` and `jobLevels` scope shapes changed (see _Migration_ below).
35
+
36
+ ### Changed
37
+
38
+ - **BREAKING — `jobLevels` is now many-to-many with departments.** A level applies to any
39
+ number of departments (empty = company-wide), replacing the single-department field:
40
+
41
+ ```diff
42
+ - jobLevels.core.departmentId: string | null
43
+ + jobLevels.core.departmentIds: string[]
44
+ ```
45
+
46
+ The create/update input follows suit (`departmentId?` → `departmentIds?: string[]`), and
47
+ `jobLevels.list({ departmentId })` filters to levels that include that department.
48
+
49
+ - **BREAKING — `employees` is now update-only, not read-only.** The scope moved from
50
+ `{ read }` to `{ read, update }`, and the readable fields were re-bucketed. A new writable
51
+ **`placement`** bucket holds `managerId`, `partnerId`, `departmentId`, `jurisdictionId`,
52
+ `jobLevelId`, `employmentType`, `isExternal`, and `timezone`; identity/contact/sensitive/
53
+ employment remain read-only (Google/HRIS-owned). Create and delete are still rejected
54
+ (onboarding requires Google Workspace provisioning).
55
+
56
+ ### Added
57
+
58
+ - **`employees.update(id, data)`** — patch the `placement` bucket, gated at compile time by a
59
+ declared `employees.update` scope and per-bucket-authorized at runtime. Writing any
60
+ read-only field (identity/contact/etc.) is rejected with `400` / `INVALID_SCOPE`.
61
+ - **`employeeProfiles` accessor** — work-arrangement records: `get(employeeId)` and
62
+ `upsert(employeeId, data)` (daily hours, probation months, overtime enabled).
63
+ - **`employeeReviews` accessor** — `list` / `create` / `update` / `delete`. The `feedback`
64
+ bucket (rating + written feedback) is confidential and approval-gated, separate from the
65
+ `core` review metadata.
66
+ - **`salaryBands.departmentId`** — bands can now be scoped to a department (null = company-wide
67
+ fallback); added to reads, create/update input, and the `list({ departmentId })` filter.
68
+ - **`genericRates.jobLevelId`** — an optional link from a generic rate to a job level; added to
69
+ reads, create input, and the `list({ jobLevelId })` filter.
70
+
71
+ ### Migration
72
+
73
+ 1. `jobLevels`: replace any `departmentId` read/write with `departmentIds` (a `string[]`; use
74
+ `[]` for a company-wide level).
75
+ 2. `employees`: change the scope from a bucket array / read-only form to
76
+ `{ read: [...], update: ['placement'] }` if the app writes placement fields; call
77
+ `nucleus.employees.update(id, { ... })` for those writes. No create/delete.
78
+ 3. New optional fields (`salaryBands.departmentId`, `genericRates.jobLevelId`) are additive —
79
+ no action needed unless you want to set or filter by them.
80
+
81
+ ## [0.2.1] - 2026-06-30
82
+
83
+ ### Changed
84
+
85
+ - **Removed `Department.titles`.** The unused `titles: string[]` field was dropped from the
86
+ `Department` type.
87
+
88
+ ### Fixed
89
+
90
+ - **Inlined resource path literals.** Internal accessors (employee-compensation, project
91
+ members) previously built request paths through private helper methods; these are now inline
92
+ string literals. No behavioural change — this keeps the paths discoverable by the OpenAPI
93
+ sync check.
94
+ - **`AddProjectMemberInput` is now a `type` alias** (was an `interface`), fixing a
95
+ snake-case body-cast type error. Field shape is unchanged.
96
+
97
+ ## [0.2.0] - 2026-06-29
98
+
99
+ Reference data, compensation writes, and communication surfaces.
100
+
101
+ ### Added
102
+
103
+ - **`jobLevels` accessor** — read the career-ladder levels (code, name, per-department scope).
104
+ - **`salaryBands` accessor** — read per-`(level, jurisdiction)` pay ranges (financial figures
105
+ are sensitivity-gated).
106
+ - **`employeeCompensation` accessor** — `list(employeeId)`, `current(employeeId)`,
107
+ `create(...)`, and `delete(employeeId, id)` for an employee's compensation history.
108
+ - **`calendar.listEvents(params)`** — query Google Calendar events for a workspace user.
109
+ - **`email.send(input)`** — send mode-aware email (live sends via Gmail; test mode is
110
+ sandboxed).
111
+ - **`timesheets.bulkCreate(...)`** — create many timesheet entries in one request.
112
+ - **Project team membership** — `projects.listMembers`, `addMember`, and `removeMember`.
113
+ - **Incremental sync** — list accessors accept `updatedSince` (ISO-8601) to return only records
114
+ changed at or after that time.
115
+ - **`projects.list({ stage })`** — filter projects by pipeline stage.
116
+
9
117
  ## [0.1.1] - 2026-06-22
10
118
 
11
119
  ### Changed
@@ -399,6 +399,7 @@ function transformGenericRate(raw) {
399
399
  if ('id' in r) out['id'] = r['id'];
400
400
  if ('title' in r) out['title'] = str(r['title']);
401
401
  if ('department' in r) out['department'] = str(r['department']);
402
+ if ('job_level_id' in r) out['jobLevelId'] = str(r['job_level_id']);
402
403
  if ('currency_code' in r) out['currencyCode'] = str(r['currency_code']);
403
404
  if ('cost_rate' in r) out['costRate'] = num(r['cost_rate']);
404
405
  if ('billable_rate' in r) out['billableRate'] = num(r['billable_rate']);
@@ -421,7 +422,10 @@ function transformJobLevel(raw) {
421
422
  if ('id' in r) out['id'] = r['id'];
422
423
  if ('name' in r) out['name'] = str(r['name']);
423
424
  if ('code' in r) out['code'] = str(r['code']);
424
- if ('department_id' in r) out['departmentId'] = str(r['department_id']);
425
+ // The API returns the join rows under `departments`; flatten to a string[] of ids.
426
+ if ('departments' in r && Array.isArray(r['departments'])) {
427
+ out['departmentIds'] = r['departments'].map((d)=>d?.['department_id']).filter((v)=>typeof v === 'string');
428
+ }
425
429
  if ('sort_order' in r) out['sortOrder'] = num(r['sort_order']) ?? 0;
426
430
  return out;
427
431
  }
@@ -431,6 +435,7 @@ function transformSalaryBand(raw) {
431
435
  const out = {};
432
436
  if ('id' in r) out['id'] = r['id'];
433
437
  if ('job_level_id' in r) out['jobLevelId'] = str(r['job_level_id']);
438
+ if ('department_id' in r) out['departmentId'] = str(r['department_id']);
434
439
  if ('jurisdiction_id' in r) out['jurisdictionId'] = str(r['jurisdiction_id']);
435
440
  if ('currency_code' in r) out['currencyCode'] = str(r['currency_code']);
436
441
  if ('min_salary' in r) out['minSalary'] = num(r['min_salary']);
@@ -547,6 +552,12 @@ function transformCalendarEvent(raw) {
547
552
  if ('location' in r) out['location'] = str(r['location']);
548
553
  if ('color' in r) out['color'] = str(r['color']);
549
554
  if ('is_published' in r) out['isPublished'] = bool(r['is_published']);
555
+ if ('jurisdiction_id' in r) out['jurisdictionId'] = str(r['jurisdiction_id']);
556
+ if ('national' in r) out['national'] = bool(r['national']);
557
+ // Expand: jurisdiction (recursively transformed)
558
+ if ('jurisdiction' in r) {
559
+ out['jurisdiction'] = r['jurisdiction'] ? transformJurisdiction(r['jurisdiction']) : null;
560
+ }
550
561
  return out;
551
562
  }
552
563
  function transformTimesheet(raw) {
@@ -607,6 +618,18 @@ class EmployeesAccessor {
607
618
  data: transformEmployee(result.data)
608
619
  };
609
620
  }
621
+ /**
622
+ * Update an employee's placement (manager, partner, department, jurisdiction, job
623
+ * level, employment type, external flag, timezone). Requires `employees` update on the
624
+ * `placement` bucket. Identity/contact/sensitive fields are Google-owned and read-only;
625
+ * employees cannot be created or deleted via the SDK.
626
+ */ async update(id, data) {
627
+ const result = await this.transport.patch(`/api/v1/data/employees/${id}`, toSnakeBody(data));
628
+ if ('error' in result) return result;
629
+ return {
630
+ data: transformEmployee(result.data)
631
+ };
632
+ }
610
633
  }
611
634
 
612
635
  function transformMember(raw) {
@@ -671,25 +694,22 @@ class ProjectsAccessor {
671
694
  // ─── Team membership (the projects `team` bucket) ──────────────────────────
672
695
  // Membership is the project↔employee join. Reads need `projects` read on `team`;
673
696
  // add/remove need `projects` update on `team` (re-adding restores a soft-deleted row).
674
- membersBase(projectId) {
675
- return `/api/v1/data/project-memberships/projects/${projectId}/members`;
676
- }
677
697
  /** List a project's team members. Requires `projects` read on the `team` bucket. */ async listMembers(projectId) {
678
- const result = await this.transport.get(this.membersBase(projectId));
698
+ const result = await this.transport.get(`/api/v1/data/project-memberships/projects/${projectId}/members`);
679
699
  if ('error' in result) return result;
680
700
  return {
681
701
  data: (result.data ?? []).map((r)=>transformMember(r))
682
702
  };
683
703
  }
684
704
  /** Add a team member. Requires `projects` update on the `team` bucket. */ async addMember(projectId, data) {
685
- const result = await this.transport.post(this.membersBase(projectId), toSnakeBody(data));
705
+ const result = await this.transport.post(`/api/v1/data/project-memberships/projects/${projectId}/members`, toSnakeBody(data));
686
706
  if ('error' in result) return result;
687
707
  return {
688
708
  data: transformMember(result.data)
689
709
  };
690
710
  }
691
711
  /** Remove a team member. Requires `projects` update on the `team` bucket. */ async removeMember(projectId, employeeId) {
692
- const result = await this.transport.del(`${this.membersBase(projectId)}/${employeeId}`);
712
+ const result = await this.transport.del(`/api/v1/data/project-memberships/projects/${projectId}/members/${employeeId}`);
693
713
  if ('error' in result) return result;
694
714
  return {
695
715
  data: null
@@ -1093,6 +1113,7 @@ class JurisdictionsAccessor {
1093
1113
  async list(params) {
1094
1114
  const query = {};
1095
1115
  if (params?.jobLevelId) query['jobLevelId'] = params.jobLevelId;
1116
+ if (params?.departmentId) query['departmentId'] = params.departmentId;
1096
1117
  if (params?.jurisdictionId) query['jurisdictionId'] = params.jurisdictionId;
1097
1118
  if (params?.page) query['page'] = params.page;
1098
1119
  if (params?.pageSize) query['pageSize'] = params.pageSize;
@@ -1396,6 +1417,7 @@ class CalendarEventsAccessor {
1396
1417
  if (params?.page) query['page'] = params.page;
1397
1418
  if (params?.pageSize) query['pageSize'] = params.pageSize;
1398
1419
  if (params?.updatedSince) query['updatedSince'] = params.updatedSince;
1420
+ if (params?.expand?.length) query['expand'] = params.expand.join(',');
1399
1421
  const result = await this.transport.getList('/api/v1/data/calendar-events', query);
1400
1422
  if ('error' in result) return result;
1401
1423
  return {
@@ -1403,8 +1425,10 @@ class CalendarEventsAccessor {
1403
1425
  meta: result.meta
1404
1426
  };
1405
1427
  }
1406
- async getById(id) {
1407
- const result = await this.transport.get(`/api/v1/data/calendar-events/${id}`);
1428
+ async getById(id, options) {
1429
+ const query = {};
1430
+ if (options?.expand?.length) query['expand'] = options.expand.join(',');
1431
+ const result = await this.transport.get(`/api/v1/data/calendar-events/${id}`, query);
1408
1432
  if ('error' in result) return result;
1409
1433
  return {
1410
1434
  data: transformCalendarEvent(result.data)
@@ -1538,7 +1562,7 @@ class TimesheetsAccessor {
1538
1562
  }
1539
1563
  }
1540
1564
 
1541
- function transform(raw) {
1565
+ function transform$2(raw) {
1542
1566
  return {
1543
1567
  id: raw['id'],
1544
1568
  employeeId: raw['employee_id'],
@@ -1560,32 +1584,111 @@ function transform(raw) {
1560
1584
  constructor(transport){
1561
1585
  this.transport = transport;
1562
1586
  }
1563
- base(employeeId) {
1564
- return `/api/v1/data/employee-compensation/employees/${employeeId}/compensation`;
1565
- }
1566
1587
  /** Full history, newest first. */ async list(employeeId) {
1567
- const result = await this.transport.get(this.base(employeeId));
1588
+ const result = await this.transport.get(`/api/v1/data/employee-compensation/employees/${employeeId}/compensation`);
1568
1589
  if ('error' in result) return result;
1569
1590
  return {
1570
- data: (result.data ?? []).map((r)=>transform(r))
1591
+ data: (result.data ?? []).map((r)=>transform$2(r))
1571
1592
  };
1572
1593
  }
1573
1594
  /** The current (open) record, or null. */ async current(employeeId) {
1574
- const result = await this.transport.get(`${this.base(employeeId)}/current`);
1595
+ const result = await this.transport.get(`/api/v1/data/employee-compensation/employees/${employeeId}/compensation/current`);
1575
1596
  if ('error' in result) return result;
1576
1597
  return {
1577
- data: result.data ? transform(result.data) : null
1598
+ data: result.data ? transform$2(result.data) : null
1578
1599
  };
1579
1600
  }
1580
1601
  /** Add a new effective-dated record (closes the prior open one). */ async create(employeeId, data) {
1581
- const result = await this.transport.post(this.base(employeeId), toSnakeBody(data));
1602
+ const result = await this.transport.post(`/api/v1/data/employee-compensation/employees/${employeeId}/compensation`, toSnakeBody(data));
1582
1603
  if ('error' in result) return result;
1583
1604
  return {
1584
- data: transform(result.data)
1605
+ data: transform$2(result.data)
1585
1606
  };
1586
1607
  }
1587
1608
  /** Soft-delete a compensation record. */ async delete(employeeId, id) {
1588
- const result = await this.transport.del(`${this.base(employeeId)}/${id}`);
1609
+ const result = await this.transport.del(`/api/v1/data/employee-compensation/employees/${employeeId}/compensation/${id}`);
1610
+ if ('error' in result) return result;
1611
+ return {
1612
+ data: null
1613
+ };
1614
+ }
1615
+ }
1616
+
1617
+ function transform$1(raw) {
1618
+ return {
1619
+ id: raw['id'],
1620
+ employeeId: raw['employee_id'],
1621
+ dailyHours: raw['daily_hours'] ?? 8,
1622
+ probationMonths: raw['probation_months'] ?? null,
1623
+ isOvertimeEnabled: raw['is_overtime_enabled'] ?? false
1624
+ };
1625
+ }
1626
+ /**
1627
+ * EmployeeProfilesAccessor — an employee's work-arrangement profile (Iteration 4).
1628
+ * Gated server-side by the `employee-profiles` scope.
1629
+ */ class EmployeeProfilesAccessor {
1630
+ constructor(transport){
1631
+ this.transport = transport;
1632
+ }
1633
+ async get(employeeId) {
1634
+ const result = await this.transport.get(`/api/v1/data/employee-profiles/employees/${employeeId}/profile`);
1635
+ if ('error' in result) return result;
1636
+ return {
1637
+ data: result.data ? transform$1(result.data) : null
1638
+ };
1639
+ }
1640
+ /** Create-or-update the profile. Requires `employee-profiles` update scope. */ async upsert(employeeId, data) {
1641
+ const result = await this.transport.put(`/api/v1/data/employee-profiles/employees/${employeeId}/profile`, toSnakeBody(data));
1642
+ if ('error' in result) return result;
1643
+ return {
1644
+ data: transform$1(result.data)
1645
+ };
1646
+ }
1647
+ }
1648
+
1649
+ function transform(raw) {
1650
+ return {
1651
+ id: raw['id'],
1652
+ employeeId: raw['employee_id'],
1653
+ reviewType: raw['review_type'],
1654
+ dueDate: raw['due_date'] ?? null,
1655
+ completedDate: raw['completed_date'] ?? null,
1656
+ status: raw['status'],
1657
+ rating: raw['rating'] ?? null,
1658
+ feedback: raw['feedback'] ?? null,
1659
+ reviewerId: raw['reviewer_id'] ?? null
1660
+ };
1661
+ }
1662
+ /**
1663
+ * EmployeeReviewsAccessor — performance reviews (Iteration 4). Gated server-side by the
1664
+ * `employee-reviews` scope; the `feedback` bucket (rating + feedback) is approval-gated.
1665
+ */ class EmployeeReviewsAccessor {
1666
+ constructor(transport){
1667
+ this.transport = transport;
1668
+ }
1669
+ async list(employeeId) {
1670
+ const result = await this.transport.get(`/api/v1/data/employee-reviews/employees/${employeeId}/reviews`);
1671
+ if ('error' in result) return result;
1672
+ return {
1673
+ data: (result.data ?? []).map((r)=>transform(r))
1674
+ };
1675
+ }
1676
+ async create(employeeId, data) {
1677
+ const result = await this.transport.post(`/api/v1/data/employee-reviews/employees/${employeeId}/reviews`, toSnakeBody(data));
1678
+ if ('error' in result) return result;
1679
+ return {
1680
+ data: transform(result.data)
1681
+ };
1682
+ }
1683
+ async update(employeeId, id, data) {
1684
+ const result = await this.transport.patch(`/api/v1/data/employee-reviews/employees/${employeeId}/reviews/${id}`, toSnakeBody(data));
1685
+ if ('error' in result) return result;
1686
+ return {
1687
+ data: transform(result.data)
1688
+ };
1689
+ }
1690
+ async delete(employeeId, id) {
1691
+ const result = await this.transport.del(`/api/v1/data/employee-reviews/employees/${employeeId}/reviews/${id}`);
1589
1692
  if ('error' in result) return result;
1590
1693
  return {
1591
1694
  data: null
@@ -1759,6 +1862,8 @@ class AuthAccessor {
1759
1862
  this.calendar = new CalendarAccessor(transport);
1760
1863
  this.email = new EmailAccessor(transport);
1761
1864
  this.employeeCompensation = new EmployeeCompensationAccessor(transport);
1865
+ this.employeeProfiles = new EmployeeProfilesAccessor(transport);
1866
+ this.employeeReviews = new EmployeeReviewsAccessor(transport);
1762
1867
  this.apps = new AppsAccessor(transport);
1763
1868
  this.files = new FilesAccessor(transport);
1764
1869
  this.auth = new AuthAccessor(config.baseUrl);
@@ -49,8 +49,6 @@ type EmployeeIdentity = {
49
49
  jobTitle: string | null;
50
50
  department: string | null;
51
51
  employmentStatus: string;
52
- employmentType: string | null;
53
- isExternal: boolean;
54
52
  };
55
53
  type EmployeeContact = {
56
54
  profile: {
@@ -62,13 +60,21 @@ type EmployeeContact = {
62
60
  type EmployeeEmployment = {
63
61
  startDate: string | null;
64
62
  weeklyCapacity: number | null;
63
+ profile: {
64
+ biography?: string | null;
65
+ };
66
+ };
67
+ /** HR-managed org placement — the only app-writable employee bucket (update-only). */
68
+ type EmployeePlacement = {
65
69
  managerId: string | null;
66
70
  partnerId: string | null;
71
+ departmentId: string | null;
72
+ jurisdictionId: string | null;
73
+ jobLevelId: string | null;
74
+ employmentType: string | null;
75
+ isExternal: boolean;
67
76
  /** IANA timezone string, e.g. "Asia/Colombo". Null when not yet set. */
68
77
  timezone: string | null;
69
- profile: {
70
- biography?: string | null;
71
- };
72
78
  };
73
79
  type EmployeeSensitive = {
74
80
  birthday: string | null;
@@ -107,10 +113,22 @@ type EmployeeBucketMap = {
107
113
  identity: EmployeeIdentity;
108
114
  contact: EmployeeContact;
109
115
  employment: EmployeeEmployment;
116
+ placement: EmployeePlacement;
110
117
  sensitive: EmployeeSensitive;
111
118
  compensation: EmployeeCompensation;
112
119
  compensation_history: EmployeeCompensationHistory;
113
120
  };
121
+ /** Body for `employees.update` — the writable `placement` fields only. */
122
+ type EmployeePlacementUpdateInput = {
123
+ managerId?: string | null;
124
+ partnerId?: string | null;
125
+ departmentId?: string | null;
126
+ jurisdictionId?: string | null;
127
+ jobLevelId?: string | null;
128
+ employmentType?: string | null;
129
+ isExternal?: boolean;
130
+ timezone?: string | null;
131
+ };
114
132
  type ProjectMembership = {
115
133
  employeeId: string;
116
134
  role: string | null;
@@ -226,6 +244,7 @@ type GenericRateCore = {
226
244
  id: string;
227
245
  title: string;
228
246
  department: string;
247
+ jobLevelId: string | null;
229
248
  currencyCode: string;
230
249
  };
231
250
  type GenericRateFinancials = {
@@ -249,7 +268,8 @@ type JobLevelCore = {
249
268
  id: string;
250
269
  name: string;
251
270
  code: string | null;
252
- departmentId: string | null;
271
+ /** Departments this level applies to. Empty = company-wide. */
272
+ departmentIds: string[];
253
273
  sortOrder: number;
254
274
  };
255
275
  type JobLevelBucketMap = {
@@ -258,6 +278,7 @@ type JobLevelBucketMap = {
258
278
  type SalaryBandCore = {
259
279
  id: string;
260
280
  jobLevelId: string;
281
+ departmentId: string | null;
261
282
  jurisdictionId: string | null;
262
283
  currencyCode: string;
263
284
  };
@@ -346,6 +367,10 @@ type CalendarEventCore = {
346
367
  location: string | null;
347
368
  color: string | null;
348
369
  isPublished: boolean;
370
+ /** Jurisdiction this event applies to (holidays); null for non-holiday events. */
371
+ jurisdictionId: string | null;
372
+ /** For holidays: national (country-level) vs region-specific. */
373
+ national: boolean;
349
374
  };
350
375
  type CalendarEventBucketMap = {
351
376
  core: CalendarEventCore;
@@ -387,12 +412,13 @@ type ResourceScope<M> = {
387
412
  update?: readonly (keyof M)[];
388
413
  delete?: boolean;
389
414
  };
390
- /** A resource Nucleus owns the writes for only `read` may be declared. */
391
- type ReadOnlyResourceScope<M> = {
415
+ /** A resource that may be read or updated, but never created/deleted (e.g. employees). */
416
+ type UpdateOnlyResourceScope<M> = {
392
417
  read?: readonly (keyof M)[];
418
+ update?: readonly (keyof M)[];
393
419
  };
394
420
  type ScopeDeclaration = {
395
- employees?: ReadOnlyResourceScope<EmployeeBucketMap>;
421
+ employees?: UpdateOnlyResourceScope<EmployeeBucketMap>;
396
422
  projects?: ResourceScope<ProjectBucketMap>;
397
423
  clients?: ResourceScope<ClientBucketMap>;
398
424
  partners?: ResourceScope<PartnerBucketMap>;
@@ -505,6 +531,10 @@ type WithTimesheetExpand<T, E extends readonly TimesheetExpandPath[], S extends
505
531
  } : unknown) & ('project' extends E[number] ? {
506
532
  project: ResolveProject<S> | null;
507
533
  } : unknown);
534
+ type CalendarEventExpandPath = 'jurisdiction';
535
+ type WithCalendarEventExpand<T, E extends readonly CalendarEventExpandPath[], S extends ScopeDeclaration> = T & ('jurisdiction' extends E[number] ? {
536
+ jurisdiction: ResolveJurisdiction<S> | null;
537
+ } : unknown);
508
538
  type LeaveCreateInput = {
509
539
  employeeId: string;
510
540
  leaveTypeId: string;
@@ -577,6 +607,7 @@ type CurrencyUpdateInput = Partial<Omit<CurrencyCreateInput, 'code'>>;
577
607
  type GenericRateCreateInput = {
578
608
  title: string;
579
609
  department: string;
610
+ jobLevelId?: string | null;
580
611
  costRate: number;
581
612
  billableRate: number;
582
613
  currencyCode?: string;
@@ -595,13 +626,15 @@ type JurisdictionUpdateInput = Partial<JurisdictionCreateInput>;
595
626
  type JobLevelCreateInput = {
596
627
  name: string;
597
628
  code?: string | null;
598
- /** Owning department; null = company-wide rung. */
599
- departmentId?: string | null;
629
+ /** Departments this level applies to. Empty/omitted = company-wide rung. */
630
+ departmentIds?: string[];
600
631
  sortOrder?: number;
601
632
  };
602
633
  type JobLevelUpdateInput = Partial<JobLevelCreateInput>;
603
634
  type SalaryBandCreateInput = {
604
635
  jobLevelId: string;
636
+ /** Null = applies to all departments. */
637
+ departmentId?: string | null;
605
638
  /** Null = applies to all jurisdictions (org-wide band). */
606
639
  jurisdictionId?: string | null;
607
640
  currencyCode?: string;
@@ -748,6 +781,13 @@ declare class EmployeesAccessor<S extends ScopeDeclaration> {
748
781
  getById<E extends readonly EmployeeExpandPath[] = never[]>(id: string, options?: {
749
782
  expand?: E;
750
783
  }): Promise<SingleResult<WithEmployeeExpand<ResolveEmployee<S>, E, S>>>;
784
+ /**
785
+ * Update an employee's placement (manager, partner, department, jurisdiction, job
786
+ * level, employment type, external flag, timezone). Requires `employees` update on the
787
+ * `placement` bucket. Identity/contact/sensitive fields are Google-owned and read-only;
788
+ * employees cannot be created or deleted via the SDK.
789
+ */
790
+ update(id: string, data: IfUpdate<S['employees'], EmployeePlacementUpdateInput>): Promise<SingleResult<ResolveEmployee<S>>>;
751
791
  }
752
792
 
753
793
  type ProjectListParams = {
@@ -769,10 +809,10 @@ interface ProjectMember {
769
809
  employeeId: string;
770
810
  role: string | null;
771
811
  }
772
- interface AddProjectMemberInput {
812
+ type AddProjectMemberInput = {
773
813
  employeeId: string;
774
814
  role?: string | null;
775
- }
815
+ };
776
816
  declare class ProjectsAccessor<S extends ScopeDeclaration> {
777
817
  private readonly transport;
778
818
  constructor(transport: NucleusTransport);
@@ -788,7 +828,6 @@ declare class ProjectsAccessor<S extends ScopeDeclaration> {
788
828
  update(id: string, data: IfUpdate<S['projects'], ProjectUpdateInput>): Promise<SingleResult<ResolveProject<S>>>;
789
829
  /** Soft-delete a project. Requires `projects.delete` scope. */
790
830
  delete(id: IfDelete<S['projects'], string>): Promise<SingleResult<null>>;
791
- private membersBase;
792
831
  /** List a project's team members. Requires `projects` read on the `team` bucket. */
793
832
  listMembers(projectId: string): Promise<SingleResult<ProjectMember[]>>;
794
833
  /** Add a team member. Requires `projects` update on the `team` bucket. */
@@ -912,6 +951,7 @@ declare class CurrenciesAccessor<S extends ScopeDeclaration> {
912
951
 
913
952
  type GenericRateListParams = {
914
953
  department?: string;
954
+ jobLevelId?: string;
915
955
  page?: number;
916
956
  pageSize?: number;
917
957
  /** ISO-8601 — return only records changed (created/updated/deleted) at or after this time (incremental sync). */
@@ -974,6 +1014,7 @@ declare class JobLevelsAccessor<S extends ScopeDeclaration> {
974
1014
 
975
1015
  type SalaryBandListParams = {
976
1016
  jobLevelId?: string;
1017
+ departmentId?: string;
977
1018
  jurisdictionId?: string;
978
1019
  page?: number;
979
1020
  pageSize?: number;
@@ -1127,12 +1168,18 @@ type CalendarEventListParams = {
1127
1168
  pageSize?: number;
1128
1169
  /** ISO-8601 — return only records changed (created/updated/deleted) at or after this time (incremental sync). */
1129
1170
  updatedSince?: string;
1171
+ /** Hydrate related resources inline (e.g. the event's jurisdiction). */
1172
+ expand?: readonly CalendarEventExpandPath[];
1130
1173
  };
1131
1174
  declare class CalendarEventsAccessor<S extends ScopeDeclaration> {
1132
1175
  private readonly transport;
1133
1176
  constructor(transport: NucleusTransport);
1134
- list(params?: CalendarEventListParams): Promise<PaginatedResult<ResolveCalendarEvent<S>>>;
1135
- getById(id: string): Promise<SingleResult<ResolveCalendarEvent<S>>>;
1177
+ list<E extends readonly CalendarEventExpandPath[] = never[]>(params?: CalendarEventListParams & {
1178
+ expand?: E;
1179
+ }): Promise<PaginatedResult<WithCalendarEventExpand<ResolveCalendarEvent<S>, E, S>>>;
1180
+ getById<E extends readonly CalendarEventExpandPath[] = never[]>(id: string, options?: {
1181
+ expand?: E;
1182
+ }): Promise<SingleResult<WithCalendarEventExpand<ResolveCalendarEvent<S>, E, S>>>;
1136
1183
  /** Create a calendar event. Requires `calendarEvents.create` scope. */
1137
1184
  create(data: IfCreate<S['calendarEvents'], CalendarEventCreateInput>): Promise<SingleResult<ResolveCalendarEvent<S>>>;
1138
1185
  /** Update a calendar event. Requires `calendarEvents.update` scope. */
@@ -1266,7 +1313,6 @@ interface EmployeeCompensationInput {
1266
1313
  declare class EmployeeCompensationAccessor {
1267
1314
  private readonly transport;
1268
1315
  constructor(transport: NucleusTransport);
1269
- private base;
1270
1316
  /** Full history, newest first. */
1271
1317
  list(employeeId: string): Promise<SingleResult<EmployeeCompensationRecord[]>>;
1272
1318
  /** The current (open) record, or null. */
@@ -1277,6 +1323,67 @@ declare class EmployeeCompensationAccessor {
1277
1323
  delete(employeeId: string, id: string): Promise<SingleResult<null>>;
1278
1324
  }
1279
1325
 
1326
+ /** Per-employee work-arrangement settings. */
1327
+ interface EmployeeProfile {
1328
+ id: string;
1329
+ employeeId: string;
1330
+ dailyHours: number;
1331
+ probationMonths: number | null;
1332
+ isOvertimeEnabled: boolean;
1333
+ }
1334
+ type EmployeeProfileInput = {
1335
+ dailyHours?: number;
1336
+ probationMonths?: number | null;
1337
+ isOvertimeEnabled?: boolean;
1338
+ };
1339
+ /**
1340
+ * EmployeeProfilesAccessor — an employee's work-arrangement profile (Iteration 4).
1341
+ * Gated server-side by the `employee-profiles` scope.
1342
+ */
1343
+ declare class EmployeeProfilesAccessor {
1344
+ private readonly transport;
1345
+ constructor(transport: NucleusTransport);
1346
+ get(employeeId: string): Promise<SingleResult<EmployeeProfile | null>>;
1347
+ /** Create-or-update the profile. Requires `employee-profiles` update scope. */
1348
+ upsert(employeeId: string, data: EmployeeProfileInput): Promise<SingleResult<EmployeeProfile>>;
1349
+ }
1350
+
1351
+ /** A performance review. `rating`/`feedback` are in the approval-gated `feedback` bucket. */
1352
+ interface EmployeeReview {
1353
+ id: string;
1354
+ employeeId: string;
1355
+ reviewType: string;
1356
+ dueDate: string | null;
1357
+ completedDate: string | null;
1358
+ status: string;
1359
+ rating: string | null;
1360
+ feedback: string | null;
1361
+ reviewerId: string | null;
1362
+ }
1363
+ type EmployeeReviewCreateInput = {
1364
+ reviewType: string;
1365
+ dueDate?: string | null;
1366
+ status?: string;
1367
+ rating?: string | null;
1368
+ feedback?: string | null;
1369
+ reviewerId?: string | null;
1370
+ };
1371
+ type EmployeeReviewUpdateInput = Partial<EmployeeReviewCreateInput> & {
1372
+ completedDate?: string | null;
1373
+ };
1374
+ /**
1375
+ * EmployeeReviewsAccessor — performance reviews (Iteration 4). Gated server-side by the
1376
+ * `employee-reviews` scope; the `feedback` bucket (rating + feedback) is approval-gated.
1377
+ */
1378
+ declare class EmployeeReviewsAccessor {
1379
+ private readonly transport;
1380
+ constructor(transport: NucleusTransport);
1381
+ list(employeeId: string): Promise<SingleResult<EmployeeReview[]>>;
1382
+ create(employeeId: string, data: EmployeeReviewCreateInput): Promise<SingleResult<EmployeeReview>>;
1383
+ update(employeeId: string, id: string, data: EmployeeReviewUpdateInput): Promise<SingleResult<EmployeeReview>>;
1384
+ delete(employeeId: string, id: string): Promise<SingleResult<null>>;
1385
+ }
1386
+
1280
1387
  /**
1281
1388
  * AppsAccessor — wraps the self-service app info endpoint (§3.8, §6).
1282
1389
  * Calls GET /api/v1/platform/apps/me with the app token.
@@ -1442,6 +1549,8 @@ declare class NucleusClient<const S extends ScopeDeclaration> {
1442
1549
  readonly email: EmailAccessor;
1443
1550
  /** Read/write an employee's compensation history (requires `employee-compensation` scope). */
1444
1551
  readonly employeeCompensation: EmployeeCompensationAccessor;
1552
+ readonly employeeProfiles: EmployeeProfilesAccessor;
1553
+ readonly employeeReviews: EmployeeReviewsAccessor;
1445
1554
  readonly apps: AppsAccessor;
1446
1555
  readonly files: FilesAccessor;
1447
1556
  readonly auth: AuthAccessor;