@company-semantics/contracts 63.2.0 → 63.4.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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +138 -6
- package/src/generated/openapi-routes.ts +2 -0
- package/src/index.ts +2 -0
- package/src/org/README.md +2 -0
- package/src/org/__tests__/org-depth.test.ts +64 -0
- package/src/org/__tests__/org-units.test.ts +10 -4
- package/src/org/company-md.ts +2 -2
- package/src/org/index.ts +4 -0
- package/src/org/org-depth.ts +51 -0
- package/src/org/org-units.ts +3 -2
- package/src/org/schemas.ts +9 -8
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = 'c73ab06a4768' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'c73ab06a476873a5f782ce5969fd3641f07372d69b4a60feade7305c428f116f' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -3778,6 +3778,23 @@ export interface paths {
|
|
|
3778
3778
|
patch?: never;
|
|
3779
3779
|
trace?: never;
|
|
3780
3780
|
};
|
|
3781
|
+
"/api/comments/threads/{threadId}": {
|
|
3782
|
+
parameters: {
|
|
3783
|
+
query?: never;
|
|
3784
|
+
header?: never;
|
|
3785
|
+
path?: never;
|
|
3786
|
+
cookie?: never;
|
|
3787
|
+
};
|
|
3788
|
+
get?: never;
|
|
3789
|
+
put?: never;
|
|
3790
|
+
post?: never;
|
|
3791
|
+
/** Soft-delete a thread of either kind (creator or subject owner) */
|
|
3792
|
+
delete: operations["deleteCommentThread"];
|
|
3793
|
+
options?: never;
|
|
3794
|
+
head?: never;
|
|
3795
|
+
patch?: never;
|
|
3796
|
+
trace?: never;
|
|
3797
|
+
};
|
|
3781
3798
|
"/api/comments/threads/{threadId}/accept-intent": {
|
|
3782
3799
|
parameters: {
|
|
3783
3800
|
query?: never;
|
|
@@ -3863,6 +3880,23 @@ export interface paths {
|
|
|
3863
3880
|
patch?: never;
|
|
3864
3881
|
trace?: never;
|
|
3865
3882
|
};
|
|
3883
|
+
"/api/comments/threads/{threadId}/revert": {
|
|
3884
|
+
parameters: {
|
|
3885
|
+
query?: never;
|
|
3886
|
+
header?: never;
|
|
3887
|
+
path?: never;
|
|
3888
|
+
cookie?: never;
|
|
3889
|
+
};
|
|
3890
|
+
get?: never;
|
|
3891
|
+
put?: never;
|
|
3892
|
+
/** Revert a decided suggestion, restoring the passage an acceptance replaced */
|
|
3893
|
+
post: operations["revertSuggestion"];
|
|
3894
|
+
delete?: never;
|
|
3895
|
+
options?: never;
|
|
3896
|
+
head?: never;
|
|
3897
|
+
patch?: never;
|
|
3898
|
+
trace?: never;
|
|
3899
|
+
};
|
|
3866
3900
|
}
|
|
3867
3901
|
export type webhooks = Record<string, never>;
|
|
3868
3902
|
export interface components {
|
|
@@ -5938,7 +5972,7 @@ export interface components {
|
|
|
5938
5972
|
/** Format: uuid */
|
|
5939
5973
|
userId: string;
|
|
5940
5974
|
/** @enum {string} */
|
|
5941
|
-
membershipRole: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner";
|
|
5975
|
+
membershipRole: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner" | "l6_unit_owner" | "l7_unit_owner" | "l8_unit_owner" | "l9_unit_owner" | "l10_unit_owner" | "l11_unit_owner" | "l12_unit_owner" | "l13_unit_owner" | "l14_unit_owner" | "l15_unit_owner" | "l16_unit_owner" | "l17_unit_owner" | "l18_unit_owner" | "l19_unit_owner" | "l20_unit_owner";
|
|
5942
5976
|
/** Format: uuid */
|
|
5943
5977
|
inheritedFromUnitId: string;
|
|
5944
5978
|
inheritedFromUnitName: string;
|
|
@@ -6074,7 +6108,7 @@ export interface components {
|
|
|
6074
6108
|
/** Format: uuid */
|
|
6075
6109
|
userId: string;
|
|
6076
6110
|
/** @enum {string} */
|
|
6077
|
-
membershipRole: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner";
|
|
6111
|
+
membershipRole: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner" | "l6_unit_owner" | "l7_unit_owner" | "l8_unit_owner" | "l9_unit_owner" | "l10_unit_owner" | "l11_unit_owner" | "l12_unit_owner" | "l13_unit_owner" | "l14_unit_owner" | "l15_unit_owner" | "l16_unit_owner" | "l17_unit_owner" | "l18_unit_owner" | "l19_unit_owner" | "l20_unit_owner";
|
|
6078
6112
|
/** @enum {string} */
|
|
6079
6113
|
status: "active" | "pending" | "removed";
|
|
6080
6114
|
/** @enum {string} */
|
|
@@ -6095,7 +6129,7 @@ export interface components {
|
|
|
6095
6129
|
/** Format: uuid */
|
|
6096
6130
|
userId: string;
|
|
6097
6131
|
/** @enum {string} */
|
|
6098
|
-
membershipRole: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner";
|
|
6132
|
+
membershipRole: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner" | "l6_unit_owner" | "l7_unit_owner" | "l8_unit_owner" | "l9_unit_owner" | "l10_unit_owner" | "l11_unit_owner" | "l12_unit_owner" | "l13_unit_owner" | "l14_unit_owner" | "l15_unit_owner" | "l16_unit_owner" | "l17_unit_owner" | "l18_unit_owner" | "l19_unit_owner" | "l20_unit_owner";
|
|
6099
6133
|
/** @enum {string} */
|
|
6100
6134
|
status: "active" | "pending" | "removed";
|
|
6101
6135
|
/** @enum {string} */
|
|
@@ -7028,6 +7062,11 @@ export interface components {
|
|
|
7028
7062
|
AnchorTraceRequest: {
|
|
7029
7063
|
lastAnchoredOffset: number;
|
|
7030
7064
|
};
|
|
7065
|
+
/** @description Acknowledges a soft-deleted thread: its id and the deletion stamp. */
|
|
7066
|
+
CommentThreadDeletedResponse: {
|
|
7067
|
+
threadId: string;
|
|
7068
|
+
deletedAt: string;
|
|
7069
|
+
};
|
|
7031
7070
|
/** @description The acceptance lease: the opaque claim token that alone can complete the accept, and how long the lease lives. */
|
|
7032
7071
|
SuggestionClaimResponse: {
|
|
7033
7072
|
claimToken: string;
|
|
@@ -7045,6 +7084,14 @@ export interface components {
|
|
|
7045
7084
|
claimToken: string;
|
|
7046
7085
|
expectedRevision: string;
|
|
7047
7086
|
};
|
|
7087
|
+
/** @description The reverted thread and the inverse document mutation that landed with it, or null when the suggestion had never been applied. */
|
|
7088
|
+
SuggestionRevertedResponse: {
|
|
7089
|
+
thread: components["schemas"]["CommentThreadSummary"];
|
|
7090
|
+
reverted: components["schemas"]["SuggestionApplied"] | null;
|
|
7091
|
+
};
|
|
7092
|
+
SuggestionRevertRequest: {
|
|
7093
|
+
expectedRevision?: string;
|
|
7094
|
+
};
|
|
7048
7095
|
/** @description Server-enforced collaboration size limits, published so clients can pre-check. */
|
|
7049
7096
|
CollabSyncLimits: {
|
|
7050
7097
|
maxUpdateBytes: number;
|
|
@@ -7205,7 +7252,7 @@ export interface components {
|
|
|
7205
7252
|
op: "insert" | "delete" | "replace";
|
|
7206
7253
|
insertedText?: string;
|
|
7207
7254
|
/** @enum {string} */
|
|
7208
|
-
status: "open" | "accepted" | "rejected";
|
|
7255
|
+
status: "open" | "accepted" | "rejected" | "reverted";
|
|
7209
7256
|
};
|
|
7210
7257
|
/** @description One comment; body is null iff soft-deleted (redacted projection with empty mentions). */
|
|
7211
7258
|
Comment: {
|
|
@@ -7246,6 +7293,8 @@ export interface components {
|
|
|
7246
7293
|
canReopen: boolean;
|
|
7247
7294
|
canDecideSuggestion: boolean;
|
|
7248
7295
|
canWithdraw: boolean;
|
|
7296
|
+
canRevertSuggestion: boolean;
|
|
7297
|
+
canDeleteThread: boolean;
|
|
7249
7298
|
};
|
|
7250
7299
|
/** @description A comment thread without its comments. */
|
|
7251
7300
|
CommentThreadSummary: {
|
|
@@ -11472,7 +11521,7 @@ export interface operations {
|
|
|
11472
11521
|
/** Format: uuid */
|
|
11473
11522
|
userId: string;
|
|
11474
11523
|
/** @enum {string} */
|
|
11475
|
-
role?: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner";
|
|
11524
|
+
role?: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner" | "l6_unit_owner" | "l7_unit_owner" | "l8_unit_owner" | "l9_unit_owner" | "l10_unit_owner" | "l11_unit_owner" | "l12_unit_owner" | "l13_unit_owner" | "l14_unit_owner" | "l15_unit_owner" | "l16_unit_owner" | "l17_unit_owner" | "l18_unit_owner" | "l19_unit_owner" | "l20_unit_owner";
|
|
11476
11525
|
};
|
|
11477
11526
|
};
|
|
11478
11527
|
};
|
|
@@ -11502,7 +11551,7 @@ export interface operations {
|
|
|
11502
11551
|
content: {
|
|
11503
11552
|
"application/json": {
|
|
11504
11553
|
/** @enum {string} */
|
|
11505
|
-
role: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner";
|
|
11554
|
+
role: "member" | "l1_unit_owner" | "l2_unit_owner" | "l3_unit_owner" | "l4_unit_owner" | "l5_unit_owner" | "l6_unit_owner" | "l7_unit_owner" | "l8_unit_owner" | "l9_unit_owner" | "l10_unit_owner" | "l11_unit_owner" | "l12_unit_owner" | "l13_unit_owner" | "l14_unit_owner" | "l15_unit_owner" | "l16_unit_owner" | "l17_unit_owner" | "l18_unit_owner" | "l19_unit_owner" | "l20_unit_owner";
|
|
11506
11555
|
};
|
|
11507
11556
|
};
|
|
11508
11557
|
};
|
|
@@ -13750,6 +13799,42 @@ export interface operations {
|
|
|
13750
13799
|
};
|
|
13751
13800
|
};
|
|
13752
13801
|
};
|
|
13802
|
+
deleteCommentThread: {
|
|
13803
|
+
parameters: {
|
|
13804
|
+
query?: never;
|
|
13805
|
+
header?: never;
|
|
13806
|
+
path: {
|
|
13807
|
+
threadId: string;
|
|
13808
|
+
};
|
|
13809
|
+
cookie?: never;
|
|
13810
|
+
};
|
|
13811
|
+
requestBody?: never;
|
|
13812
|
+
responses: {
|
|
13813
|
+
/** @description Thread soft-deleted; the response acknowledges the id and the deletion stamp. Accepted suggestion text stays in the document */
|
|
13814
|
+
200: {
|
|
13815
|
+
headers: {
|
|
13816
|
+
[name: string]: unknown;
|
|
13817
|
+
};
|
|
13818
|
+
content: {
|
|
13819
|
+
"application/json": components["schemas"]["CommentThreadDeletedResponse"];
|
|
13820
|
+
};
|
|
13821
|
+
};
|
|
13822
|
+
/** @description Thread not found, already deleted, or the caller is neither its creator nor a subject owner */
|
|
13823
|
+
404: {
|
|
13824
|
+
headers: {
|
|
13825
|
+
[name: string]: unknown;
|
|
13826
|
+
};
|
|
13827
|
+
content?: never;
|
|
13828
|
+
};
|
|
13829
|
+
/** @description A live acceptance lease is held on the suggestion */
|
|
13830
|
+
409: {
|
|
13831
|
+
headers: {
|
|
13832
|
+
[name: string]: unknown;
|
|
13833
|
+
};
|
|
13834
|
+
content?: never;
|
|
13835
|
+
};
|
|
13836
|
+
};
|
|
13837
|
+
};
|
|
13753
13838
|
acceptSuggestionIntent: {
|
|
13754
13839
|
parameters: {
|
|
13755
13840
|
query?: never;
|
|
@@ -13963,4 +14048,51 @@ export interface operations {
|
|
|
13963
14048
|
};
|
|
13964
14049
|
};
|
|
13965
14050
|
};
|
|
14051
|
+
revertSuggestion: {
|
|
14052
|
+
parameters: {
|
|
14053
|
+
query?: never;
|
|
14054
|
+
header?: never;
|
|
14055
|
+
path: {
|
|
14056
|
+
threadId: string;
|
|
14057
|
+
};
|
|
14058
|
+
cookie?: never;
|
|
14059
|
+
};
|
|
14060
|
+
requestBody: {
|
|
14061
|
+
content: {
|
|
14062
|
+
"application/json": components["schemas"]["SuggestionRevertRequest"];
|
|
14063
|
+
};
|
|
14064
|
+
};
|
|
14065
|
+
responses: {
|
|
14066
|
+
/** @description Suggestion reverted (terminal; thread stays resolved). On an accepted suggestion the inverse edit landed in the document on the SAME transaction and is reported; on a rejected one `reverted` is null */
|
|
14067
|
+
200: {
|
|
14068
|
+
headers: {
|
|
14069
|
+
[name: string]: unknown;
|
|
14070
|
+
};
|
|
14071
|
+
content: {
|
|
14072
|
+
"application/json": components["schemas"]["SuggestionRevertedResponse"];
|
|
14073
|
+
};
|
|
14074
|
+
};
|
|
14075
|
+
/** @description Malformed expectedRevision, or an accepted suggestion reverted without one */
|
|
14076
|
+
400: {
|
|
14077
|
+
headers: {
|
|
14078
|
+
[name: string]: unknown;
|
|
14079
|
+
};
|
|
14080
|
+
content?: never;
|
|
14081
|
+
};
|
|
14082
|
+
/** @description Thread not found, or the caller may not decide suggestions on it */
|
|
14083
|
+
404: {
|
|
14084
|
+
headers: {
|
|
14085
|
+
[name: string]: unknown;
|
|
14086
|
+
};
|
|
14087
|
+
content?: never;
|
|
14088
|
+
};
|
|
14089
|
+
/** @description Terminal, never retry: not a suggestion thread, still open, already reverted, a claim is on the row, the document moved past expectedRevision, or the inserted text cannot be found or no longer reads as inserted — re-read the document and decide again */
|
|
14090
|
+
409: {
|
|
14091
|
+
headers: {
|
|
14092
|
+
[name: string]: unknown;
|
|
14093
|
+
};
|
|
14094
|
+
content?: never;
|
|
14095
|
+
};
|
|
14096
|
+
};
|
|
14097
|
+
};
|
|
13966
14098
|
}
|
|
@@ -24,6 +24,7 @@ export const openApiRoutes = {
|
|
|
24
24
|
'/api/chats/{id}/pin': ['DELETE', 'POST'],
|
|
25
25
|
'/api/comments': ['GET'],
|
|
26
26
|
'/api/comments/threads': ['POST'],
|
|
27
|
+
'/api/comments/threads/{threadId}': ['DELETE'],
|
|
27
28
|
'/api/comments/threads/{threadId}/accept-apply': ['POST'],
|
|
28
29
|
'/api/comments/threads/{threadId}/accept-complete': ['POST'],
|
|
29
30
|
'/api/comments/threads/{threadId}/accept-intent': ['POST'],
|
|
@@ -33,6 +34,7 @@ export const openApiRoutes = {
|
|
|
33
34
|
'/api/comments/threads/{threadId}/reject': ['POST'],
|
|
34
35
|
'/api/comments/threads/{threadId}/reopen': ['POST'],
|
|
35
36
|
'/api/comments/threads/{threadId}/resolve': ['POST'],
|
|
37
|
+
'/api/comments/threads/{threadId}/revert': ['POST'],
|
|
36
38
|
'/api/comments/{commentId}': ['DELETE', 'PATCH'],
|
|
37
39
|
'/api/company-md/access-requests/{id}/approve': ['POST'],
|
|
38
40
|
'/api/company-md/access-requests/{id}/deny': ['POST'],
|
package/src/index.ts
CHANGED
package/src/org/README.md
CHANGED
|
@@ -193,6 +193,8 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
193
193
|
- `MissingAtNextLevel` _(type)_
|
|
194
194
|
- `MissingAtNextLevelSchema` — Active users home directly at this unit who have no level-(depth+1) home assignment within its subtree.
|
|
195
195
|
- `ORG_UNITS_ROUTES` — Canonical route path constants for the `/api/org-units` surface.
|
|
196
|
+
- `ORG_UNIT_ABSOLUTE_MAX_DEPTH` — Hard ceiling mirrored by the `org_units_depth_max` CHECK; ingest-only past designed.
|
|
197
|
+
- `ORG_UNIT_DESIGNED_MAX_DEPTH` — Depth a human may build from scratch; the UI ceiling for a fresh org.
|
|
196
198
|
- `Occupancy` _(type)_
|
|
197
199
|
- `OccupancySchema`
|
|
198
200
|
- `OccupancyStatus` _(type)_
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ORG_UNIT_ABSOLUTE_MAX_DEPTH,
|
|
4
|
+
ORG_UNIT_DESIGNED_MAX_DEPTH,
|
|
5
|
+
} from "../../index.js";
|
|
6
|
+
import {
|
|
7
|
+
OrgLevelConfigSchema,
|
|
8
|
+
OrgUnitTreeNodeSchema,
|
|
9
|
+
UnitMembershipRoleSchema,
|
|
10
|
+
} from "../schemas.js";
|
|
11
|
+
|
|
12
|
+
const UUID_A = "11111111-1111-4111-8111-111111111111";
|
|
13
|
+
|
|
14
|
+
describe("org-unit depth ceilings", () => {
|
|
15
|
+
it("designed stays below absolute and both reach the package root", () => {
|
|
16
|
+
expect(ORG_UNIT_DESIGNED_MAX_DEPTH).toBe(5);
|
|
17
|
+
expect(ORG_UNIT_ABSOLUTE_MAX_DEPTH).toBe(20);
|
|
18
|
+
expect(ORG_UNIT_DESIGNED_MAX_DEPTH).toBeLessThan(
|
|
19
|
+
ORG_UNIT_ABSOLUTE_MAX_DEPTH,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("OrgLevelConfigSchema accepts every depth up to the absolute ceiling", () => {
|
|
24
|
+
const row = (depth: number) => ({
|
|
25
|
+
orgId: UUID_A,
|
|
26
|
+
depth,
|
|
27
|
+
label: "Pod",
|
|
28
|
+
labelPlural: "Pods",
|
|
29
|
+
icon: "users",
|
|
30
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
31
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
32
|
+
});
|
|
33
|
+
expect(
|
|
34
|
+
OrgLevelConfigSchema.safeParse(row(ORG_UNIT_DESIGNED_MAX_DEPTH + 1))
|
|
35
|
+
.success,
|
|
36
|
+
).toBe(true);
|
|
37
|
+
expect(
|
|
38
|
+
OrgLevelConfigSchema.safeParse(row(ORG_UNIT_ABSOLUTE_MAX_DEPTH)).success,
|
|
39
|
+
).toBe(true);
|
|
40
|
+
expect(
|
|
41
|
+
OrgLevelConfigSchema.safeParse(row(ORG_UNIT_ABSOLUTE_MAX_DEPTH + 1))
|
|
42
|
+
.success,
|
|
43
|
+
).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("OrgUnitTreeNodeSchema bounds depth by the absolute ceiling, not the designed one", () => {
|
|
47
|
+
const shape = OrgUnitTreeNodeSchema.shape.depth;
|
|
48
|
+
expect(shape.safeParse(ORG_UNIT_DESIGNED_MAX_DEPTH + 1).success).toBe(true);
|
|
49
|
+
expect(shape.safeParse(ORG_UNIT_ABSOLUTE_MAX_DEPTH).success).toBe(true);
|
|
50
|
+
expect(shape.safeParse(ORG_UNIT_ABSOLUTE_MAX_DEPTH + 1).success).toBe(
|
|
51
|
+
false,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("carries one unit-owner membership role per depth up to the absolute ceiling", () => {
|
|
56
|
+
const owners = UnitMembershipRoleSchema.options.filter(
|
|
57
|
+
(r) => r !== "member",
|
|
58
|
+
);
|
|
59
|
+
expect(owners).toHaveLength(ORG_UNIT_ABSOLUTE_MAX_DEPTH);
|
|
60
|
+
for (let depth = 1; depth <= ORG_UNIT_ABSOLUTE_MAX_DEPTH; depth++) {
|
|
61
|
+
expect(owners).toContain(`l${depth}_unit_owner`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -90,7 +90,7 @@ describe("OrgUnitSchema", () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
describe("OrgUnitTreeNodeSchema", () => {
|
|
93
|
-
it("requires depth 1..
|
|
93
|
+
it("requires depth 1..20 (absolute ceiling)", () => {
|
|
94
94
|
const base = {
|
|
95
95
|
...makeUnit(),
|
|
96
96
|
depth: 3,
|
|
@@ -102,7 +102,10 @@ describe("OrgUnitTreeNodeSchema", () => {
|
|
|
102
102
|
};
|
|
103
103
|
expect(() => OrgUnitTreeNodeSchema.parse(base)).not.toThrow();
|
|
104
104
|
expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 0 })).toThrow();
|
|
105
|
-
expect(() =>
|
|
105
|
+
expect(() =>
|
|
106
|
+
OrgUnitTreeNodeSchema.parse({ ...base, depth: 6 }),
|
|
107
|
+
).not.toThrow();
|
|
108
|
+
expect(() => OrgUnitTreeNodeSchema.parse({ ...base, depth: 21 })).toThrow();
|
|
106
109
|
});
|
|
107
110
|
|
|
108
111
|
it("requires a non-negative openRoleCount", () => {
|
|
@@ -277,7 +280,7 @@ describe("OrgUnitRelationshipSchema", () => {
|
|
|
277
280
|
});
|
|
278
281
|
|
|
279
282
|
describe("OrgLevelConfigSchema", () => {
|
|
280
|
-
it("enforces depth 1..
|
|
283
|
+
it("enforces depth 1..20, non-empty-when-present label/labelPlural, and icon enum", () => {
|
|
281
284
|
const entry = {
|
|
282
285
|
orgId: UUID_B,
|
|
283
286
|
depth: 2,
|
|
@@ -296,7 +299,10 @@ describe("OrgLevelConfigSchema", () => {
|
|
|
296
299
|
icon: null,
|
|
297
300
|
}),
|
|
298
301
|
).not.toThrow();
|
|
299
|
-
expect(() =>
|
|
302
|
+
expect(() =>
|
|
303
|
+
OrgLevelConfigSchema.parse({ ...entry, depth: 6 }),
|
|
304
|
+
).not.toThrow();
|
|
305
|
+
expect(() => OrgLevelConfigSchema.parse({ ...entry, depth: 21 })).toThrow();
|
|
300
306
|
expect(() => OrgLevelConfigSchema.parse({ ...entry, label: "" })).toThrow();
|
|
301
307
|
expect(() =>
|
|
302
308
|
OrgLevelConfigSchema.parse({ ...entry, labelPlural: "" }),
|
package/src/org/company-md.ts
CHANGED
|
@@ -144,9 +144,9 @@ export interface CompanyMdNodeIdentity {
|
|
|
144
144
|
* docs/goals/sources/maps that are not owned by a unit).
|
|
145
145
|
*
|
|
146
146
|
* Prefer this over `level` for structural reasoning. `level` is retained for
|
|
147
|
-
* backward compatibility and is @deprecated.
|
|
147
|
+
* backward compatibility and is @deprecated. Bounded 1..`ORG_UNIT_ABSOLUTE_MAX_DEPTH`.
|
|
148
148
|
*/
|
|
149
|
-
readonly depth:
|
|
149
|
+
readonly depth: number | null;
|
|
150
150
|
readonly parentId: string | null;
|
|
151
151
|
readonly visibility: CompanyMdVisibility;
|
|
152
152
|
/**
|
package/src/org/index.ts
CHANGED
|
@@ -498,6 +498,10 @@ export type {
|
|
|
498
498
|
OrgTreeResponse,
|
|
499
499
|
} from "./org-units";
|
|
500
500
|
export { ORG_UNITS_ROUTES } from "./org-units";
|
|
501
|
+
export {
|
|
502
|
+
ORG_UNIT_DESIGNED_MAX_DEPTH,
|
|
503
|
+
ORG_UNIT_ABSOLUTE_MAX_DEPTH,
|
|
504
|
+
} from "./org-depth";
|
|
501
505
|
export {
|
|
502
506
|
OrgUnitClassificationSchema,
|
|
503
507
|
OrgUnitSyncModeSchema,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org-unit tree depth ceilings.
|
|
3
|
+
*
|
|
4
|
+
* Depth counts the org root as 1 (`nlevel(org_units.path)`). Two ceilings,
|
|
5
|
+
* deliberately distinct (backend ADR: org-unit depth, designed vs absolute):
|
|
6
|
+
*
|
|
7
|
+
* - {@link ORG_UNIT_DESIGNED_MAX_DEPTH} is the depth a human can build from
|
|
8
|
+
* scratch. The UI's "+ New child", "Add level" and drag-reparent, and the
|
|
9
|
+
* server's default (`designed`) depth policy, never deepen an org past
|
|
10
|
+
* `max(DESIGNED, the org's current deepest unit)`.
|
|
11
|
+
* - {@link ORG_UNIT_ABSOLUTE_MAX_DEPTH} mirrors the database CHECK
|
|
12
|
+
* (`org_units_depth_max`). Only ingest (structure inference apply, HRIS sync,
|
|
13
|
+
* org-chart import) runs under the `absolute` policy and may deepen an org
|
|
14
|
+
* past the designed depth, up to this bound. Nothing exceeds it.
|
|
15
|
+
*
|
|
16
|
+
* Both are compile-time constants, not per-org configuration.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Depth a human may build from scratch; the UI ceiling for a fresh org. */
|
|
20
|
+
export const ORG_UNIT_DESIGNED_MAX_DEPTH = 5;
|
|
21
|
+
|
|
22
|
+
/** Hard ceiling mirrored by the `org_units_depth_max` CHECK; ingest-only past designed. */
|
|
23
|
+
export const ORG_UNIT_ABSOLUTE_MAX_DEPTH = 20;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The positional unit-owner membership roles, one per depth 1..absolute. Spelled
|
|
27
|
+
* out rather than generated so each member stays a string literal type; the
|
|
28
|
+
* schema that consumes it (`UnitMembershipRoleSchema`) adds `member` in front.
|
|
29
|
+
*/
|
|
30
|
+
export const UNIT_OWNER_MEMBERSHIP_ROLES = [
|
|
31
|
+
"l1_unit_owner",
|
|
32
|
+
"l2_unit_owner",
|
|
33
|
+
"l3_unit_owner",
|
|
34
|
+
"l4_unit_owner",
|
|
35
|
+
"l5_unit_owner",
|
|
36
|
+
"l6_unit_owner",
|
|
37
|
+
"l7_unit_owner",
|
|
38
|
+
"l8_unit_owner",
|
|
39
|
+
"l9_unit_owner",
|
|
40
|
+
"l10_unit_owner",
|
|
41
|
+
"l11_unit_owner",
|
|
42
|
+
"l12_unit_owner",
|
|
43
|
+
"l13_unit_owner",
|
|
44
|
+
"l14_unit_owner",
|
|
45
|
+
"l15_unit_owner",
|
|
46
|
+
"l16_unit_owner",
|
|
47
|
+
"l17_unit_owner",
|
|
48
|
+
"l18_unit_owner",
|
|
49
|
+
"l19_unit_owner",
|
|
50
|
+
"l20_unit_owner",
|
|
51
|
+
] as const;
|
package/src/org/org-units.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* - `path` is an ltree string maintained by a DB trigger; never written from
|
|
19
19
|
* application code.
|
|
20
20
|
* - Depth is derived from `path`. `OrgLevelConfig.depth` is only a key for
|
|
21
|
-
* per-depth labels and policy — not authoritative structure.
|
|
21
|
+
* per-depth labels and policy — not authoritative structure. Ceilings live in
|
|
22
|
+
* `./org-depth` (designed 5, absolute 20; ingest-only past designed).
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
/** Semantic classification — projection-only, never drives traversal. */
|
|
@@ -113,7 +114,7 @@ export type { OrgUnitTreeResponse as OrgTreeResponse } from "./schemas";
|
|
|
113
114
|
export type OrgUnitErrorCode =
|
|
114
115
|
/** newParent.path contains unit.path — would create a cycle. */
|
|
115
116
|
| "CYCLE_BLOCKED"
|
|
116
|
-
/**
|
|
117
|
+
/** Create/reparent would push the subtree past the effective depth ceiling (`./org-depth`). */
|
|
117
118
|
| "DEPTH_EXCEEDED"
|
|
118
119
|
/** Target parent is archived; reparenting into archived subtrees is forbidden. */
|
|
119
120
|
| "TARGET_ARCHIVED"
|
package/src/org/schemas.ts
CHANGED
|
@@ -14,6 +14,10 @@ import { z } from "zod";
|
|
|
14
14
|
import { CursorPageSchema, IsoDateTime } from "../api/primitives";
|
|
15
15
|
import { OrgChartRoleSchema } from "../permissions/orgchart-roles";
|
|
16
16
|
import { PositionRefSchema } from "../identity/position-ref";
|
|
17
|
+
import {
|
|
18
|
+
ORG_UNIT_ABSOLUTE_MAX_DEPTH,
|
|
19
|
+
UNIT_OWNER_MEMBERSHIP_ROLES,
|
|
20
|
+
} from "./org-depth";
|
|
17
21
|
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
23
|
// Sub-schemas
|
|
@@ -1496,15 +1500,12 @@ export const OrgUnitRelationshipRoleSchema = z.enum([
|
|
|
1496
1500
|
/**
|
|
1497
1501
|
* Positional slot a member holds in the org-unit tree. ORTHOGONAL to the
|
|
1498
1502
|
* `OrgChartRole` policy vocabulary (this is an INPUT to authority resolution,
|
|
1499
|
-
* not the derived policy role). Level is encoded in the value (l1…
|
|
1503
|
+
* not the derived policy role). Level is encoded in the value (l1…l20, one
|
|
1504
|
+
* per depth up to `ORG_UNIT_ABSOLUTE_MAX_DEPTH`).
|
|
1500
1505
|
*/
|
|
1501
1506
|
export const UnitMembershipRoleSchema = z.enum([
|
|
1502
1507
|
"member",
|
|
1503
|
-
|
|
1504
|
-
"l2_unit_owner",
|
|
1505
|
-
"l3_unit_owner",
|
|
1506
|
-
"l4_unit_owner",
|
|
1507
|
-
"l5_unit_owner",
|
|
1508
|
+
...UNIT_OWNER_MEMBERSHIP_ROLES,
|
|
1508
1509
|
]);
|
|
1509
1510
|
|
|
1510
1511
|
export type UnitMembershipRole = z.infer<typeof UnitMembershipRoleSchema>;
|
|
@@ -1579,7 +1580,7 @@ export const MissingAtNextLevelSchema = z.object({
|
|
|
1579
1580
|
});
|
|
1580
1581
|
|
|
1581
1582
|
export const OrgUnitTreeNodeSchema = OrgUnitSchema.extend({
|
|
1582
|
-
depth: z.number().int().min(1).max(
|
|
1583
|
+
depth: z.number().int().min(1).max(ORG_UNIT_ABSOLUTE_MAX_DEPTH),
|
|
1583
1584
|
hasChildren: z.boolean(),
|
|
1584
1585
|
/**
|
|
1585
1586
|
* WHAT it counts is the `.describe()` below — the single field-level
|
|
@@ -1871,7 +1872,7 @@ export const OrgLevelIconSchema = z.enum([
|
|
|
1871
1872
|
|
|
1872
1873
|
export const OrgLevelConfigSchema = z.object({
|
|
1873
1874
|
orgId: z.string().uuid(),
|
|
1874
|
-
depth: z.number().int().min(1).max(
|
|
1875
|
+
depth: z.number().int().min(1).max(ORG_UNIT_ABSOLUTE_MAX_DEPTH),
|
|
1875
1876
|
label: z.string().min(1).nullable(),
|
|
1876
1877
|
labelPlural: z.string().min(1).nullable(),
|
|
1877
1878
|
icon: OrgLevelIconSchema.nullable(),
|