@company-semantics/contracts 57.0.0 → 58.1.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 +4 -4
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +12 -0
- package/src/index.ts +52 -0
- package/src/integrations/README.md +4 -0
- package/src/integrations/schemas.ts +26 -0
- package/src/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +207 -0
- package/src/notifications/__tests__/fixtures.ts +9 -0
- package/src/notifications/__tests__/org-invite.test.ts +75 -0
- package/src/notifications/__tests__/render-snapshot.test.ts +8 -0
- package/src/notifications/kinds/org-invite.ts +27 -12
- package/src/notifications/payloads.ts +7 -0
- package/src/org/README.md +38 -0
- package/src/org/__tests__/canonical-facts.test.ts +118 -0
- package/src/org/__tests__/structure-inference.test.ts +392 -0
- package/src/org/__tests__/structure-provenance.test.ts +187 -0
- package/src/org/canonical-facts.ts +94 -1
- package/src/org/index.ts +54 -0
- package/src/org/schemas.ts +23 -0
- package/src/org/structure-inference.ts +521 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ExistingPlacementFactSchema,
|
|
5
|
+
ExistingUnitFactSchema,
|
|
6
|
+
StructureProposalOriginSchema,
|
|
7
|
+
StructureAcceptedBySchema,
|
|
8
|
+
StructureAuthoritySchema,
|
|
9
|
+
StructureProvenanceSchema,
|
|
10
|
+
StructureUnitAuthoritySchema,
|
|
11
|
+
mayOverrideAuthority,
|
|
12
|
+
} from "../structure-inference.js";
|
|
13
|
+
import type { StructureAuthority } from "../structure-inference.js";
|
|
14
|
+
|
|
15
|
+
const USER_ID = "33333333-3333-4333-8333-333333333333";
|
|
16
|
+
const UNIT_ID = "44444444-4444-4444-8444-444444444444";
|
|
17
|
+
|
|
18
|
+
/** v1: the AI proposed it and an admin clicked Apply. */
|
|
19
|
+
const aiProposedHumanAccepted = {
|
|
20
|
+
proposalOrigin: "ai_inference" as const,
|
|
21
|
+
acceptedBy: { kind: "user" as const, userId: USER_ID },
|
|
22
|
+
authority: "human_confirmed" as const,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("StructureProposalOriginSchema", () => {
|
|
26
|
+
it("names the three ways a structural fact can come to be proposed", () => {
|
|
27
|
+
for (const origin of ["ai_inference", "hris_source", "manual"]) {
|
|
28
|
+
expect(StructureProposalOriginSchema.parse(origin)).toBe(origin);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("has no user_override member — that value is what the axes replace", () => {
|
|
33
|
+
expect(() =>
|
|
34
|
+
StructureProposalOriginSchema.parse("user_override"),
|
|
35
|
+
).toThrow();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("StructureAcceptedBySchema", () => {
|
|
40
|
+
it("distinguishes a person signing off from a policy deciding", () => {
|
|
41
|
+
expect(
|
|
42
|
+
StructureAcceptedBySchema.parse({ kind: "user", userId: USER_ID }),
|
|
43
|
+
).toEqual({ kind: "user", userId: USER_ID });
|
|
44
|
+
expect(StructureAcceptedBySchema.parse({ kind: "system_policy" })).toEqual({
|
|
45
|
+
kind: "system_policy",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("refuses an unattributed acceptance — a null acceptor is not an answer", () => {
|
|
50
|
+
expect(() => StructureAcceptedBySchema.parse(null)).toThrow();
|
|
51
|
+
expect(() => StructureAcceptedBySchema.parse({ kind: "user" })).toThrow();
|
|
52
|
+
expect(() =>
|
|
53
|
+
StructureAcceptedBySchema.parse({ kind: "user", userId: "not-a-uuid" }),
|
|
54
|
+
).toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("StructureAuthoritySchema", () => {
|
|
59
|
+
it("human confirmed outranks ai inferred outranks hris source", () => {
|
|
60
|
+
const descending: StructureAuthority[] = [
|
|
61
|
+
"human_confirmed",
|
|
62
|
+
"ai_inferred",
|
|
63
|
+
"hris_source",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Every authority may overwrite itself and everything below it...
|
|
67
|
+
for (let i = 0; i < descending.length; i += 1) {
|
|
68
|
+
for (let j = i; j < descending.length; j += 1) {
|
|
69
|
+
expect(mayOverrideAuthority(descending[i]!, descending[j]!)).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ...and nothing below may overwrite something above it.
|
|
73
|
+
for (let i = 0; i < descending.length; i += 1) {
|
|
74
|
+
for (let j = 0; j < i; j += 1) {
|
|
75
|
+
expect(mayOverrideAuthority(descending[i]!, descending[j]!)).toBe(
|
|
76
|
+
false,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("lets an equal authority correct itself, so a first write is not frozen", () => {
|
|
83
|
+
expect(mayOverrideAuthority("hris_source", "hris_source")).toBe(true);
|
|
84
|
+
expect(mayOverrideAuthority("human_confirmed", "human_confirmed")).toBe(
|
|
85
|
+
true,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("stops an HRIS sync reverting an accepted placement", () => {
|
|
90
|
+
expect(mayOverrideAuthority("hris_source", "human_confirmed")).toBe(false);
|
|
91
|
+
expect(mayOverrideAuthority("hris_source", "ai_inferred")).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("is the same ladder the engine reads, not a second copy of it", () => {
|
|
95
|
+
expect(StructureAuthoritySchema).toBe(StructureUnitAuthoritySchema);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("StructureProvenanceSchema", () => {
|
|
100
|
+
it("records acceptance without erasing how the fact originated", () => {
|
|
101
|
+
const parsed = StructureProvenanceSchema.parse(aiProposedHumanAccepted);
|
|
102
|
+
expect(parsed.authority).toBe("human_confirmed");
|
|
103
|
+
// The origin survives acceptance — this is the whole defect being fixed.
|
|
104
|
+
expect(parsed.proposalOrigin).toBe("ai_inference");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("keeps the axes orthogonal: same origin, different acceptor, different authority", () => {
|
|
108
|
+
const autoAccepted = StructureProvenanceSchema.parse({
|
|
109
|
+
proposalOrigin: "ai_inference",
|
|
110
|
+
acceptedBy: { kind: "system_policy" },
|
|
111
|
+
authority: "ai_inferred",
|
|
112
|
+
});
|
|
113
|
+
expect(autoAccepted.proposalOrigin).toBe(
|
|
114
|
+
aiProposedHumanAccepted.proposalOrigin,
|
|
115
|
+
);
|
|
116
|
+
expect(autoAccepted.authority).not.toBe(aiProposedHumanAccepted.authority);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("round-trips the manual and HRIS rows of the provenance table", () => {
|
|
120
|
+
const manual = {
|
|
121
|
+
proposalOrigin: "manual" as const,
|
|
122
|
+
acceptedBy: { kind: "user" as const, userId: USER_ID },
|
|
123
|
+
authority: "human_confirmed" as const,
|
|
124
|
+
};
|
|
125
|
+
const feed = {
|
|
126
|
+
proposalOrigin: "hris_source" as const,
|
|
127
|
+
acceptedBy: { kind: "system_policy" as const },
|
|
128
|
+
authority: "hris_source" as const,
|
|
129
|
+
};
|
|
130
|
+
expect(StructureProvenanceSchema.parse(manual)).toEqual(manual);
|
|
131
|
+
expect(StructureProvenanceSchema.parse(feed)).toEqual(feed);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("carries its authority onto the existing fact a human accepted", () => {
|
|
135
|
+
const accepted = StructureProvenanceSchema.parse(aiProposedHumanAccepted);
|
|
136
|
+
const placement = ExistingPlacementFactSchema.parse({
|
|
137
|
+
personId: "p1",
|
|
138
|
+
unitId: UNIT_ID,
|
|
139
|
+
authority: accepted.authority,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// A human-accepted placement is an ANCHOR, never a revisitable AI guess —
|
|
143
|
+
// recording it as ai_inferred is the failure this vocabulary exists to close.
|
|
144
|
+
expect(placement.authority).toBe("human_confirmed");
|
|
145
|
+
expect(placement.authority).not.toBe("ai_inferred");
|
|
146
|
+
expect(mayOverrideAuthority("ai_inferred", placement.authority)).toBe(
|
|
147
|
+
false,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("reserves ai_inferred for acceptance by policy, with nobody in the loop", () => {
|
|
152
|
+
const autoAccepted = StructureProvenanceSchema.parse({
|
|
153
|
+
proposalOrigin: "ai_inference",
|
|
154
|
+
acceptedBy: { kind: "system_policy" },
|
|
155
|
+
authority: "ai_inferred",
|
|
156
|
+
});
|
|
157
|
+
expect(autoAccepted.acceptedBy.kind).toBe("system_policy");
|
|
158
|
+
|
|
159
|
+
const unit = ExistingUnitFactSchema.parse({
|
|
160
|
+
unitId: UNIT_ID,
|
|
161
|
+
name: "Platform",
|
|
162
|
+
parentUnitId: null,
|
|
163
|
+
depth: 0,
|
|
164
|
+
authority: autoAccepted.authority,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Nobody signed off, so the engine may still reorganize it and a later
|
|
168
|
+
// human decision overwrites it.
|
|
169
|
+
expect(unit.authority).toBe("ai_inferred");
|
|
170
|
+
expect(mayOverrideAuthority("human_confirmed", unit.authority)).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("requires all three axes — a partial record cannot answer who signed off", () => {
|
|
174
|
+
expect(() =>
|
|
175
|
+
StructureProvenanceSchema.parse({
|
|
176
|
+
proposalOrigin: "ai_inference",
|
|
177
|
+
authority: "human_confirmed",
|
|
178
|
+
}),
|
|
179
|
+
).toThrow();
|
|
180
|
+
expect(() =>
|
|
181
|
+
StructureProvenanceSchema.parse({
|
|
182
|
+
acceptedBy: { kind: "system_policy" },
|
|
183
|
+
authority: "hris_source",
|
|
184
|
+
}),
|
|
185
|
+
).toThrow();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* source said" and "what the org graph becomes": adapters translate their
|
|
8
8
|
* proprietary payloads into this vocabulary, and the reconciler consumes it to
|
|
9
9
|
* create/supersede internal Person / OrgUnit / Position / Occupancy / reporting
|
|
10
|
-
* records. See contracts ADR-CONT-085 and control ADR-CTRL-183
|
|
10
|
+
* records. See contracts ADR-CONT-085 and control ADR-CTRL-183; ADR-CONTRACTS-140
|
|
11
|
+
* amends ADR-CONT-085 with the sixth fact kind (`unresolvedManagers`), the
|
|
12
|
+
* required `status` on a position, and the major-bump consequence.
|
|
11
13
|
*
|
|
12
14
|
* Two properties make these facts distinct from the in-graph schemas they mirror
|
|
13
15
|
* ({@link PersonSchema}, {@link PositionSchema}, {@link OccupancySchema}, …):
|
|
@@ -81,6 +83,34 @@ export const CanonicalOrgUnitSchema = z.object({
|
|
|
81
83
|
});
|
|
82
84
|
export type CanonicalOrgUnit = z.infer<typeof CanonicalOrgUnitSchema>;
|
|
83
85
|
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// CanonicalPositionStatus — seat existence AS REPORTED by the source
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Seat-existence as the SOURCE reported it. `filled` is the ordinary case; an
|
|
92
|
+
* adapter that cannot say must say `filled` rather than omit the field.
|
|
93
|
+
*
|
|
94
|
+
* This is a REPORTED claim at the ingest seam, deliberately narrower than the
|
|
95
|
+
* in-graph {@link PositionStatusSchema} lifecycle (which also has `closed`): a
|
|
96
|
+
* source describing its current workforce reports seats that exist, and seat
|
|
97
|
+
* retirement is a reconciliation outcome, not something a directory states.
|
|
98
|
+
*
|
|
99
|
+
* It does NOT contradict the occupancy invariant that in-graph VACANCY is
|
|
100
|
+
* derived from the absence of an active {@link OccupancySchema} (see
|
|
101
|
+
* `occupancy.ts`). This field records what the source SAID about the seat; the
|
|
102
|
+
* reconciler still derives vacancy from occupancy facts and never reads a
|
|
103
|
+
* `vacant` flag off a Position.
|
|
104
|
+
*/
|
|
105
|
+
export const CanonicalPositionStatusSchema = z.enum([
|
|
106
|
+
"planned",
|
|
107
|
+
"open",
|
|
108
|
+
"filled",
|
|
109
|
+
]);
|
|
110
|
+
export type CanonicalPositionStatus = z.infer<
|
|
111
|
+
typeof CanonicalPositionStatusSchema
|
|
112
|
+
>;
|
|
113
|
+
|
|
84
114
|
// ---------------------------------------------------------------------------
|
|
85
115
|
// CanonicalPosition — a normalized seat reported by an external source
|
|
86
116
|
// ---------------------------------------------------------------------------
|
|
@@ -99,6 +129,11 @@ export const CanonicalPositionSchema = z.object({
|
|
|
99
129
|
title: z.string().min(1),
|
|
100
130
|
/** External id of the org unit this seat lives in. */
|
|
101
131
|
unitExternalSourceId: z.string().min(1),
|
|
132
|
+
/**
|
|
133
|
+
* Seat existence as the source reported it. REQUIRED — pre-launch we ship no
|
|
134
|
+
* back-compat optionality; an adapter that cannot say must say `filled`.
|
|
135
|
+
*/
|
|
136
|
+
status: CanonicalPositionStatusSchema,
|
|
102
137
|
/** Provenance of this fact (truth hierarchy + supersede model). */
|
|
103
138
|
provenance: FactProvenanceSchema,
|
|
104
139
|
});
|
|
@@ -156,6 +191,58 @@ export type CanonicalReportingEdge = z.infer<
|
|
|
156
191
|
typeof CanonicalReportingEdgeSchema
|
|
157
192
|
>;
|
|
158
193
|
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// CanonicalUnresolvedManager — a supervisory reference that resolved to nothing
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Why a supervisory reference could not be resolved to a seat in this batch.
|
|
200
|
+
*
|
|
201
|
+
* `unknown` is the default and asserts NOTHING beyond "we could not resolve
|
|
202
|
+
* it". Only `vacant_position` claims the company has an unfilled seat, and only
|
|
203
|
+
* when the source actually says so — "we could not resolve this reference" and
|
|
204
|
+
* "the company has an open requisition" are different organizational claims,
|
|
205
|
+
* and their causes (permissions, termination, stale data, scope limits, a
|
|
206
|
+
* hidden record, a real vacancy) are not interchangeable.
|
|
207
|
+
*/
|
|
208
|
+
export const CanonicalUnresolvedManagerResolutionSchema = z.enum([
|
|
209
|
+
"unknown",
|
|
210
|
+
"vacant_position",
|
|
211
|
+
"inactive_employee",
|
|
212
|
+
"not_visible",
|
|
213
|
+
]);
|
|
214
|
+
export type CanonicalUnresolvedManagerResolution = z.infer<
|
|
215
|
+
typeof CanonicalUnresolvedManagerResolutionSchema
|
|
216
|
+
>;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A supervisory reference the source reported that we could NOT resolve to a
|
|
220
|
+
* seat in this batch.
|
|
221
|
+
*
|
|
222
|
+
* Emitting this fact is what keeps an unresolvable supervisor from becoming a
|
|
223
|
+
* SILENT drop: the report seats that pointed at it stay traceable instead of
|
|
224
|
+
* surfacing as unexplained roots in the org graph. It never mints a seat by
|
|
225
|
+
* itself — see {@link CanonicalUnresolvedManagerResolutionSchema} for why only
|
|
226
|
+
* `vacant_position` licenses that.
|
|
227
|
+
*/
|
|
228
|
+
export const CanonicalUnresolvedManagerSchema = z.object({
|
|
229
|
+
/** Stable id for this unresolved reference in the origin system. */
|
|
230
|
+
externalSourceId: z.string().min(1),
|
|
231
|
+
/** Origin system naming this id — OPEN string. */
|
|
232
|
+
externalSourceSystem: z.string().min(1),
|
|
233
|
+
/** The supervisor identifier the source gave us and we could not resolve. */
|
|
234
|
+
externalManagerId: z.string().min(1),
|
|
235
|
+
/** External ids of the report seats pointing at this unresolved manager. */
|
|
236
|
+
reportPositionExternalSourceIds: z.array(z.string().min(1)),
|
|
237
|
+
/** Why it did not resolve; defaults to the non-committal `unknown`. */
|
|
238
|
+
resolution: CanonicalUnresolvedManagerResolutionSchema.default("unknown"),
|
|
239
|
+
/** Provenance of this fact (truth hierarchy + supersede model). */
|
|
240
|
+
provenance: FactProvenanceSchema,
|
|
241
|
+
});
|
|
242
|
+
export type CanonicalUnresolvedManager = z.infer<
|
|
243
|
+
typeof CanonicalUnresolvedManagerSchema
|
|
244
|
+
>;
|
|
245
|
+
|
|
159
246
|
// ---------------------------------------------------------------------------
|
|
160
247
|
// CanonicalFacts — the batch envelope an adapter emits for reconciliation
|
|
161
248
|
// ---------------------------------------------------------------------------
|
|
@@ -165,6 +252,10 @@ export type CanonicalReportingEdge = z.infer<
|
|
|
165
252
|
* person, org unit, position, occupancy, and reporting edge it observed, each
|
|
166
253
|
* carrying external identity + provenance. The reconciler consumes this whole
|
|
167
254
|
* envelope to create/supersede internal org-graph records.
|
|
255
|
+
*
|
|
256
|
+
* `unresolvedManagers` is the batch's honesty channel: a supervisory reference
|
|
257
|
+
* the adapter could not resolve is REPORTED here rather than dropped, so the
|
|
258
|
+
* count of observed seats minus placed edges is fully explained.
|
|
168
259
|
*/
|
|
169
260
|
export const CanonicalFactsSchema = z.object({
|
|
170
261
|
/** Every person observed in this batch. */
|
|
@@ -177,5 +268,7 @@ export const CanonicalFactsSchema = z.object({
|
|
|
177
268
|
occupancies: z.array(CanonicalOccupancySchema),
|
|
178
269
|
/** Every seat-to-manager-seat reporting edge observed in this batch. */
|
|
179
270
|
reportingEdges: z.array(CanonicalReportingEdgeSchema),
|
|
271
|
+
/** Every supervisory reference this batch could NOT resolve to a seat. */
|
|
272
|
+
unresolvedManagers: z.array(CanonicalUnresolvedManagerSchema),
|
|
180
273
|
});
|
|
181
274
|
export type CanonicalFacts = z.infer<typeof CanonicalFactsSchema>;
|
package/src/org/index.ts
CHANGED
|
@@ -208,20 +208,74 @@ export type {
|
|
|
208
208
|
export {
|
|
209
209
|
CanonicalPersonSchema,
|
|
210
210
|
CanonicalOrgUnitSchema,
|
|
211
|
+
CanonicalPositionStatusSchema,
|
|
211
212
|
CanonicalPositionSchema,
|
|
212
213
|
CanonicalOccupancySchema,
|
|
213
214
|
CanonicalReportingEdgeSchema,
|
|
215
|
+
CanonicalUnresolvedManagerResolutionSchema,
|
|
216
|
+
CanonicalUnresolvedManagerSchema,
|
|
214
217
|
CanonicalFactsSchema,
|
|
215
218
|
} from "./canonical-facts";
|
|
216
219
|
export type {
|
|
217
220
|
CanonicalPerson,
|
|
218
221
|
CanonicalOrgUnit,
|
|
222
|
+
CanonicalPositionStatus,
|
|
219
223
|
CanonicalPosition,
|
|
220
224
|
CanonicalOccupancy,
|
|
221
225
|
CanonicalReportingEdge,
|
|
226
|
+
CanonicalUnresolvedManagerResolution,
|
|
227
|
+
CanonicalUnresolvedManager,
|
|
222
228
|
CanonicalFacts,
|
|
223
229
|
} from "./canonical-facts";
|
|
224
230
|
|
|
231
|
+
// Org-structure inference: the snapshot the engine reasons over and the
|
|
232
|
+
// proposed desired state it returns. Evidence is TYPED so the validator can
|
|
233
|
+
// enforce boundary and naming rules mechanically; a proposed unit carries
|
|
234
|
+
// targetUnitId so a rename preserves durable identity; every person in the
|
|
235
|
+
// snapshot receives exactly one outcome. (PRD-00955)
|
|
236
|
+
//
|
|
237
|
+
// Accepted structural facts additionally carry StructureProvenance: three
|
|
238
|
+
// ORTHOGONAL axes — proposalOrigin, acceptedBy and authority — so that clicking
|
|
239
|
+
// Apply on an AI proposal makes it human_confirmed without making its origin
|
|
240
|
+
// manual. Precedence runs on `authority` alone, via mayOverrideAuthority.
|
|
241
|
+
// (PRD-00956)
|
|
242
|
+
export {
|
|
243
|
+
StructureEvidenceSchema,
|
|
244
|
+
TOPOLOGY_ONLY_EVIDENCE_KINDS,
|
|
245
|
+
StructureUnitAuthoritySchema,
|
|
246
|
+
StructureProposalOriginSchema,
|
|
247
|
+
StructureAcceptedBySchema,
|
|
248
|
+
StructureAuthoritySchema,
|
|
249
|
+
StructureProvenanceSchema,
|
|
250
|
+
mayOverrideAuthority,
|
|
251
|
+
ProposedOrgUnitSchema,
|
|
252
|
+
PersonStructureOutcomeSchema,
|
|
253
|
+
StructureReviewItemSchema,
|
|
254
|
+
StructureProposalSchema,
|
|
255
|
+
StructurePersonFactSchema,
|
|
256
|
+
StructureReportingFactSchema,
|
|
257
|
+
ExistingUnitFactSchema,
|
|
258
|
+
ExistingPlacementFactSchema,
|
|
259
|
+
StructureInferenceSnapshotSchema,
|
|
260
|
+
} from "./structure-inference";
|
|
261
|
+
export type {
|
|
262
|
+
StructureEvidence,
|
|
263
|
+
StructureUnitAuthority,
|
|
264
|
+
StructureProposalOrigin,
|
|
265
|
+
StructureAcceptedBy,
|
|
266
|
+
StructureAuthority,
|
|
267
|
+
StructureProvenance,
|
|
268
|
+
ProposedOrgUnit,
|
|
269
|
+
PersonStructureOutcome,
|
|
270
|
+
StructureReviewItem,
|
|
271
|
+
StructureProposal,
|
|
272
|
+
StructurePersonFact,
|
|
273
|
+
StructureReportingFact,
|
|
274
|
+
ExistingUnitFact,
|
|
275
|
+
ExistingPlacementFact,
|
|
276
|
+
StructureInferenceSnapshot,
|
|
277
|
+
} from "./structure-inference";
|
|
278
|
+
|
|
225
279
|
// Canonical OrgUnit tree ordering (PRD-00506)
|
|
226
280
|
export type { TreeOrderableNode } from "./tree-ordering";
|
|
227
281
|
export { orderTreeNodes } from "./tree-ordering";
|
package/src/org/schemas.ts
CHANGED
|
@@ -1192,6 +1192,29 @@ export type UpdateInviteBatchRequest = z.infer<
|
|
|
1192
1192
|
*/
|
|
1193
1193
|
export const SendInviteBatchRequestSchema = z.object({
|
|
1194
1194
|
expectedRevision: z.number().int().nonnegative(),
|
|
1195
|
+
/**
|
|
1196
|
+
* The reviewer's optional note, carried into every invitation this send mails
|
|
1197
|
+
* and attributed to the inviter (`NotificationPayloads['org.invite'].message`).
|
|
1198
|
+
*
|
|
1199
|
+
* It rides the SEND rather than the batch on purpose: it authorizes nothing,
|
|
1200
|
+
* so it has no business bumping a revision that every open review reads as
|
|
1201
|
+
* "read this again". Not persisted — the invitation is the only thing that
|
|
1202
|
+
* ever carries it.
|
|
1203
|
+
*/
|
|
1204
|
+
message: z.string().max(2000).optional(),
|
|
1205
|
+
/**
|
|
1206
|
+
* Per-person role overrides, keyed by `personId` — the reviewer set a role for
|
|
1207
|
+
* ONE recipient rather than for the batch.
|
|
1208
|
+
*
|
|
1209
|
+
* The batch's own `role` remains what everyone else is invited as; a key here
|
|
1210
|
+
* replaces it for that person only. A key naming somebody outside the batch,
|
|
1211
|
+
* or somebody it is no longer sending to, is INERT rather than an error: the
|
|
1212
|
+
* live directory re-check at send time already decides who is mailed, and a
|
|
1213
|
+
* stale override describes a person nobody is inviting.
|
|
1214
|
+
*/
|
|
1215
|
+
roleByPersonId: z
|
|
1216
|
+
.record(z.string().uuid(), z.enum(["admin", "member"]))
|
|
1217
|
+
.optional(),
|
|
1195
1218
|
});
|
|
1196
1219
|
|
|
1197
1220
|
export type SendInviteBatchRequest = z.infer<
|