@company-semantics/contracts 62.0.0 → 62.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": "62.0.0",
3
+ "version": "62.2.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 = '5b50d2608eb0' as const;
3
- export const SPEC_HASH_FULL = '5b50d2608eb090a8a020d1219d8f3ba9d99f825c72fd5f5695f9d2542f32b8a3' as const;
2
+ export const SPEC_HASH = '8e4ad15ffd00' as const;
3
+ export const SPEC_HASH_FULL = '8e4ad15ffd00b9b83f7adb26bd7bfdff0c03994756b3022b9dbc27f3ba9f7852' as const;
@@ -5646,8 +5646,11 @@ export interface components {
5646
5646
  updatedAt: string;
5647
5647
  depth: number;
5648
5648
  hasChildren: boolean;
5649
+ /** @description Accounts with a home assignment, rolled up over this unit's subtree. Legitimately 0 for a fully staffed department in an HRIS-imported org, where almost nobody has signed in — use placedPeopleCount for headcount. */
5649
5650
  memberCount: number;
5651
+ /** @description Active open roles (`open` + `hiring`) rolled up over this unit's subtree. Vacant seats only — an HRIS-imported seat is `lifecycle_owner='import:hris'` and is counted by neither this nor memberCount. */
5650
5652
  openRoleCount: number;
5653
+ /** @description Distinct people holding a live canonical seat in this unit's subtree, whether or not they have an account. Deliberately does NOT filter positions.status: an occupied `closed` seat still draws a card on the org chart, so a filter here would disagree with it. */
5651
5654
  placedPeopleCount: number;
5652
5655
  missingAtNextLevel: {
5653
5656
  count: number;
@@ -5795,8 +5798,11 @@ export interface components {
5795
5798
  updatedAt: string;
5796
5799
  depth: number;
5797
5800
  hasChildren: boolean;
5801
+ /** @description Accounts with a home assignment, rolled up over this unit's subtree. Legitimately 0 for a fully staffed department in an HRIS-imported org, where almost nobody has signed in — use placedPeopleCount for headcount. */
5798
5802
  memberCount: number;
5803
+ /** @description Active open roles (`open` + `hiring`) rolled up over this unit's subtree. Vacant seats only — an HRIS-imported seat is `lifecycle_owner='import:hris'` and is counted by neither this nor memberCount. */
5799
5804
  openRoleCount: number;
5805
+ /** @description Distinct people holding a live canonical seat in this unit's subtree, whether or not they have an account. Deliberately does NOT filter positions.status: an occupied `closed` seat still draws a card on the org chart, so a filter here would disagree with it. */
5800
5806
  placedPeopleCount: number;
5801
5807
  missingAtNextLevel: {
5802
5808
  count: number;
package/src/org/README.md CHANGED
@@ -335,6 +335,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
335
335
  - `RemoveMemberRequest` _(type)_ — Request payload for removing a member from the workspace.
336
336
  - `RemoveMemberResponse` _(type)_
337
337
  - `RemoveMemberResponseSchema`
338
+ - `ReportingErrorCode` _(type)_
339
+ - `ReportingErrorCodeSchema` — Structured codes for reporting-line mutation rejections, surfaced as `ErrorResponse.meta.reportingCode` by…
338
340
  - `ResolveOrgDivergenceResult` _(type)_
339
341
  - `ResolveOrgDivergenceResultSchema`
340
342
  - `RoleCatalogEntry` _(type)_ — Entry in the RBAC roles catalog (GET /api/rbac/roles).
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
2
2
  import {
3
3
  PositionReportingSchema,
4
4
  PositionReportingRelationshipTypeSchema,
5
+ ReportingErrorCodeSchema,
5
6
  } from "../position-reporting.js";
6
7
 
7
8
  const validEdge = {
@@ -86,3 +87,17 @@ describe("PositionReportingRelationshipTypeSchema", () => {
86
87
  ).toBe(false);
87
88
  });
88
89
  });
90
+
91
+ describe("Enum exhaustiveness", () => {
92
+ it("ReportingErrorCodeSchema enumerates all 3 known codes", () => {
93
+ const codes = [
94
+ "REPORTING_CYCLE_BLOCKED",
95
+ "ENDPOINT_NOT_ON_CHART",
96
+ "SELF_REPORT_BLOCKED",
97
+ ];
98
+ for (const code of codes) {
99
+ expect(() => ReportingErrorCodeSchema.parse(code)).not.toThrow();
100
+ }
101
+ expect(() => ReportingErrorCodeSchema.parse("OTHER")).toThrow();
102
+ });
103
+ });
package/src/org/index.ts CHANGED
@@ -115,10 +115,12 @@ export type { PositionStatus, Position } from "./positions";
115
115
  export {
116
116
  PositionReportingRelationshipTypeSchema,
117
117
  PositionReportingSchema,
118
+ ReportingErrorCodeSchema,
118
119
  } from "./position-reporting";
119
120
  export type {
120
121
  PositionReportingRelationshipType,
121
122
  PositionReporting,
123
+ ReportingErrorCode,
122
124
  } from "./position-reporting";
123
125
 
124
126
  // Source authority: per-field ownership — who may WRITE a given entity field.
@@ -61,3 +61,27 @@ export const PositionReportingSchema = z.object({
61
61
  provenance: FactProvenanceSchema.optional(),
62
62
  });
63
63
  export type PositionReporting = z.infer<typeof PositionReportingSchema>;
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // ReportingErrorCode — structured rejection codes for reporting-line mutations
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Structured codes for reporting-line mutation rejections, surfaced as
71
+ * `ErrorResponse.meta.reportingCode` by `PUT /api/reporting/positions/{id}/manager`
72
+ * and its preview twin. The top-level `error` field only carries the error
73
+ * class (`CONFLICT`, `VALIDATION_ERROR`), so this code is the one channel that
74
+ * lets a client tell a cycle 409 from a not-on-chart 409 and key per-code copy
75
+ * off it (the `OrgUnitErrorCode` lesson). The closed set ships all known codes
76
+ * at once: adding a member later is a breaking change for consumers holding an
77
+ * exhaustive `Record<ReportingErrorCode, …>`.
78
+ */
79
+ export const ReportingErrorCodeSchema = z.enum([
80
+ // 409 — the edge would close a reporting cycle on the position graph.
81
+ "REPORTING_CYCLE_BLOCKED",
82
+ // 409 — an endpoint of the edge has no seat on the org chart yet.
83
+ "ENDPOINT_NOT_ON_CHART",
84
+ // 400 — a seat (or member) cannot report to itself.
85
+ "SELF_REPORT_BLOCKED",
86
+ ]);
87
+ export type ReportingErrorCode = z.infer<typeof ReportingErrorCodeSchema>;
@@ -1582,36 +1582,48 @@ export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
1582
1582
  depth: z.number().int().min(1).max(5),
1583
1583
  hasChildren: z.boolean(),
1584
1584
  /**
1585
- * ACCOUNTS homed in this unit's subtreea rollup of `home_assignments`,
1586
- * whose `user_id` is NOT NULL. A person who has never signed in therefore
1587
- * cannot contribute to it, and a freshly HRIS-synced org is ~93 people and
1588
- * one account, so this is legitimately 0 for a fully staffed department.
1585
+ * WHAT it counts is the `.describe()` below the single field-level
1586
+ * description, which reaches `openapi/backend.yaml` and the generated API
1587
+ * types. This comment carries only WHY, so the two cannot drift into
1588
+ * disagreeing.
1589
1589
  *
1590
- * That is not a defect: it is the right population for anything that needs an
1591
- * account to act on — the share dialog's fanout, the unit-scoped members
1592
- * table. For "how many people are in this department", use
1593
- * {@link placedPeopleCount}. The two answer different questions and
1594
- * legitimately disagree (ADR-CONTRACTS-150, ADR-BE-657).
1590
+ * Why accounts: it is the right population for anything that needs an account
1591
+ * to act on — the share dialog's fanout, the unit-scoped members table. It is
1592
+ * backed by `home_assignments`, whose `user_id` is NOT NULL, so an accountless
1593
+ * person structurally cannot appear in it. Deliberately NOT redefined and NOT
1594
+ * deprecated; {@link placedPeopleCount} answers the headcount question, and the
1595
+ * two legitimately disagree (ADR-CONTRACTS-150, ADR-BE-657).
1595
1596
  */
1596
- memberCount: z.number().int().min(0),
1597
+ memberCount: z
1598
+ .number()
1599
+ .int()
1600
+ .min(0)
1601
+ .describe(
1602
+ "Accounts with a home assignment, rolled up over this unit's subtree. Legitimately 0 for a fully staffed department in an HRIS-imported org, where almost nobody has signed in — use placedPeopleCount for headcount.",
1603
+ ),
1604
+ /** Drives the "(N + M open)" count split in the people surfaces. */
1605
+ openRoleCount: z
1606
+ .number()
1607
+ .int()
1608
+ .min(0)
1609
+ .describe(
1610
+ "Active open roles (`open` + `hiring`) rolled up over this unit's subtree. Vacant seats only — an HRIS-imported seat is `lifecycle_owner='import:hris'` and is counted by neither this nor memberCount.",
1611
+ ),
1597
1612
  /**
1598
- * Active open roles (`open` + `hiring`) rolled up over this unit's subtree,
1599
- * the same way `memberCount` rolls up real members. Drives the "(N + M open)"
1600
- * count split in the people surfaces. `memberCount` stays real-members-only.
1613
+ * Predicate: `placement_role='canonical'`, `status <> 'ended'`, keyed by
1614
+ * `positions.unit_id`. Placement and home are independent graphs
1615
+ * (ADR-CTRL-159), so a person seated here may be homed elsewhere and vice
1616
+ * versa. The `positions.status` non-filter stated in the `.describe()` below
1617
+ * is deliberate and load-bearing — the backend pins it with a test whose name
1618
+ * says so (ADR-BE-657).
1601
1619
  */
1602
- openRoleCount: z.number().int().min(0),
1603
- /**
1604
- * Distinct PEOPLE holding a live canonical seat in this unit's subtree —
1605
- * `placement_role='canonical'`, `status <> 'ended'`, keyed by
1606
- * `positions.unit_id`. Account linkage is not consulted, so this is the count
1607
- * that describes an imported workforce.
1608
- *
1609
- * Placement and home are independent graphs (ADR-CTRL-159): a person seated
1610
- * here may be homed elsewhere, and vice versa. Deliberately does NOT filter
1611
- * `positions.status` — an occupied `closed` seat still draws a card on the
1612
- * org chart, and a filter here would disagree with it (ADR-BE-657).
1613
- */
1614
- placedPeopleCount: z.number().int().min(0),
1620
+ placedPeopleCount: z
1621
+ .number()
1622
+ .int()
1623
+ .min(0)
1624
+ .describe(
1625
+ "Distinct people holding a live canonical seat in this unit's subtree, whether or not they have an account. Deliberately does NOT filter positions.status: an occupied `closed` seat still draws a card on the org chart, so a filter here would disagree with it.",
1626
+ ),
1615
1627
  missingAtNextLevel: MissingAtNextLevelSchema.nullable(),
1616
1628
  });
1617
1629