@org-design-studio/core 0.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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist-core/core.d.mts +642 -0
- package/dist-core/core.d.ts +642 -0
- package/dist-core/core.js +2323 -0
- package/dist-core/core.js.map +1 -0
- package/dist-core/core.mjs +2280 -0
- package/dist-core/core.mjs.map +1 -0
- package/docs/API.md +202 -0
- package/package.json +36 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORG DESIGN STUDIO — Core data model.
|
|
3
|
+
*
|
|
4
|
+
* All workforce data is processed client-side. The Employee record mirrors the
|
|
5
|
+
* upload template; optional fields degrade gracefully throughout analytics.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Seat occupancy of a position record. Absent = "filled" — every record
|
|
9
|
+
* persisted or imported before this field existed represents a person in a
|
|
10
|
+
* seat. "vacant" = budgeted seat, currently unoccupied (open requisition);
|
|
11
|
+
* "tbh" = planned/to-be-hired seat (e.g. created in a scenario). For
|
|
12
|
+
* vacant/TBH seats, total_cost carries the BUDGETED cost of the seat.
|
|
13
|
+
*/
|
|
14
|
+
type PositionStatus = "filled" | "vacant" | "tbh";
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a record's seat status, guarding legacy/malformed data: anything
|
|
17
|
+
* other than an explicit "vacant"/"tbh" reads as "filled".
|
|
18
|
+
*/
|
|
19
|
+
declare function positionStatus(e: Pick<Employee, "position_status">): PositionStatus;
|
|
20
|
+
interface Employee {
|
|
21
|
+
employee_id: string;
|
|
22
|
+
employee_name: string;
|
|
23
|
+
role_title: string;
|
|
24
|
+
manager_id: string | null;
|
|
25
|
+
manager_name?: string;
|
|
26
|
+
department: string;
|
|
27
|
+
function: string;
|
|
28
|
+
business_unit: string;
|
|
29
|
+
location: string;
|
|
30
|
+
country: string;
|
|
31
|
+
grade: string;
|
|
32
|
+
level: number;
|
|
33
|
+
employment_type: string;
|
|
34
|
+
fte: number;
|
|
35
|
+
salary: number;
|
|
36
|
+
bonus: number;
|
|
37
|
+
total_cost: number;
|
|
38
|
+
status: string;
|
|
39
|
+
/** Seat occupancy — absent = filled. Read via positionStatus(), never directly. */
|
|
40
|
+
position_status?: PositionStatus;
|
|
41
|
+
tenure?: number;
|
|
42
|
+
gender?: string;
|
|
43
|
+
age_band?: string;
|
|
44
|
+
performance_rating?: string;
|
|
45
|
+
potential_rating?: string;
|
|
46
|
+
critical_role?: boolean;
|
|
47
|
+
role_family?: string;
|
|
48
|
+
role_type?: string;
|
|
49
|
+
cost_center?: string;
|
|
50
|
+
legal_entity?: string;
|
|
51
|
+
contract_type?: string;
|
|
52
|
+
remote_status?: string;
|
|
53
|
+
skills?: string;
|
|
54
|
+
succession_risk?: string;
|
|
55
|
+
attrition_risk?: string;
|
|
56
|
+
change_impact?: string;
|
|
57
|
+
comments?: string;
|
|
58
|
+
}
|
|
59
|
+
/** Derived, computed per-employee attributes (never stored, always recomputed). */
|
|
60
|
+
interface EmployeeNode {
|
|
61
|
+
employee: Employee;
|
|
62
|
+
layer: number;
|
|
63
|
+
directReports: string[];
|
|
64
|
+
span: number;
|
|
65
|
+
filledSpan: number;
|
|
66
|
+
downstreamHeadcount: number;
|
|
67
|
+
downstreamCost: number;
|
|
68
|
+
isManager: boolean;
|
|
69
|
+
isManagerOfOne: boolean;
|
|
70
|
+
managesOnlyManagers: boolean;
|
|
71
|
+
managerChain: string[];
|
|
72
|
+
}
|
|
73
|
+
interface OrgTree {
|
|
74
|
+
nodes: Map<string, EmployeeNode>;
|
|
75
|
+
roots: string[];
|
|
76
|
+
maxLayer: number;
|
|
77
|
+
orphans: string[];
|
|
78
|
+
circular: string[];
|
|
79
|
+
}
|
|
80
|
+
interface SpanTargetByLevel {
|
|
81
|
+
level: number;
|
|
82
|
+
label: string;
|
|
83
|
+
min: number;
|
|
84
|
+
max: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Transition / one-time cost model for restructuring scenarios.
|
|
88
|
+
* Used to net year-1 savings against the cost of executing the change.
|
|
89
|
+
*/
|
|
90
|
+
interface TransitionAssumptions {
|
|
91
|
+
severanceMonthsByLevel: {
|
|
92
|
+
maxLevel: number;
|
|
93
|
+
months: number;
|
|
94
|
+
}[];
|
|
95
|
+
backfillPct: number;
|
|
96
|
+
oneTimeChangeCostPerMove: number;
|
|
97
|
+
effectiveMonth: number;
|
|
98
|
+
countrySeveranceMultipliers: Record<string, number>;
|
|
99
|
+
}
|
|
100
|
+
/** Month labels for the transition effective-month control (1-indexed). */
|
|
101
|
+
declare const MONTH_NAMES: readonly ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
102
|
+
/**
|
|
103
|
+
* Default seniority-noise tokens stripped from role titles before duplication
|
|
104
|
+
* grouping. Deliberately excludes "assistant" / "lead" / "associate" — those
|
|
105
|
+
* usually denote genuinely different jobs ("Assistant Manager" ≠ "Manager").
|
|
106
|
+
*/
|
|
107
|
+
declare const DEFAULT_TITLE_NOISE_WORDS: string[];
|
|
108
|
+
interface Assumptions {
|
|
109
|
+
narrowSpanThreshold: number;
|
|
110
|
+
wideSpanThreshold: number;
|
|
111
|
+
targetSpansByLevel: SpanTargetByLevel[];
|
|
112
|
+
targetMaxLayers: number;
|
|
113
|
+
delayeringSavingPct: number;
|
|
114
|
+
duplicateRoleSavingPct: number;
|
|
115
|
+
spanOptPremiumPct: number;
|
|
116
|
+
locationCostMultipliers: Record<string, number>;
|
|
117
|
+
currency: string;
|
|
118
|
+
currencySymbol: string;
|
|
119
|
+
scoringWeights: ScenarioScoringWeights;
|
|
120
|
+
benchmarkLayersForSize: {
|
|
121
|
+
maxHeadcount: number;
|
|
122
|
+
layers: number;
|
|
123
|
+
}[];
|
|
124
|
+
transitionAssumptions: TransitionAssumptions;
|
|
125
|
+
aiAugmentationByRoleFamily: Record<string, "high" | "medium" | "low">;
|
|
126
|
+
titleNoiseWords: string[];
|
|
127
|
+
highCostThreshold: number;
|
|
128
|
+
}
|
|
129
|
+
interface ScenarioScoringWeights {
|
|
130
|
+
financialImpact: number;
|
|
131
|
+
spanImprovement: number;
|
|
132
|
+
layerReduction: number;
|
|
133
|
+
complexityReduction: number;
|
|
134
|
+
changeRisk: number;
|
|
135
|
+
implementationEase: number;
|
|
136
|
+
}
|
|
137
|
+
declare const DEFAULT_ASSUMPTIONS: Assumptions;
|
|
138
|
+
type IssueSeverity = "critical" | "warning" | "info";
|
|
139
|
+
interface DataQualityIssue {
|
|
140
|
+
id: string;
|
|
141
|
+
severity: IssueSeverity;
|
|
142
|
+
category: string;
|
|
143
|
+
message: string;
|
|
144
|
+
employeeIds: string[];
|
|
145
|
+
}
|
|
146
|
+
interface DataQualityReport {
|
|
147
|
+
score: number;
|
|
148
|
+
band: "Strong" | "Usable" | "Needs cleanup" | "Poor";
|
|
149
|
+
issues: DataQualityIssue[];
|
|
150
|
+
counts: {
|
|
151
|
+
critical: number;
|
|
152
|
+
warning: number;
|
|
153
|
+
info: number;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
type ScenarioChange = {
|
|
157
|
+
type: "move";
|
|
158
|
+
employeeId: string;
|
|
159
|
+
newManagerId: string;
|
|
160
|
+
description: string;
|
|
161
|
+
} | {
|
|
162
|
+
type: "remove_role";
|
|
163
|
+
employeeId: string;
|
|
164
|
+
reportsTo: "managers_manager" | "specific" | "orphan";
|
|
165
|
+
newManagerId?: string;
|
|
166
|
+
description: string;
|
|
167
|
+
} | {
|
|
168
|
+
type: "add_role";
|
|
169
|
+
employee: Employee;
|
|
170
|
+
description: string;
|
|
171
|
+
} | {
|
|
172
|
+
type: "merge_teams";
|
|
173
|
+
sourceManagerId: string;
|
|
174
|
+
targetManagerId: string;
|
|
175
|
+
description: string;
|
|
176
|
+
} | {
|
|
177
|
+
type: "delayer";
|
|
178
|
+
layer: number;
|
|
179
|
+
scope: {
|
|
180
|
+
field: "function" | "business_unit" | "all";
|
|
181
|
+
value?: string;
|
|
182
|
+
};
|
|
183
|
+
description: string;
|
|
184
|
+
} | {
|
|
185
|
+
type: "location_shift";
|
|
186
|
+
employeeIds: string[];
|
|
187
|
+
targetLocation: string;
|
|
188
|
+
costMultiplier: number;
|
|
189
|
+
description: string;
|
|
190
|
+
} | {
|
|
191
|
+
type: "cost_change";
|
|
192
|
+
employeeId: string;
|
|
193
|
+
newTotalCost: number;
|
|
194
|
+
description: string;
|
|
195
|
+
} | {
|
|
196
|
+
type: "fill_role";
|
|
197
|
+
employeeId: string;
|
|
198
|
+
employeeName: string;
|
|
199
|
+
description: string;
|
|
200
|
+
};
|
|
201
|
+
interface ChangeLogEntry {
|
|
202
|
+
id: string;
|
|
203
|
+
timestamp: string;
|
|
204
|
+
change: ScenarioChange;
|
|
205
|
+
affectedCount: number;
|
|
206
|
+
costBefore: number;
|
|
207
|
+
costAfter: number;
|
|
208
|
+
headcountBefore: number;
|
|
209
|
+
headcountAfter: number;
|
|
210
|
+
layersBefore: number;
|
|
211
|
+
layersAfter: number;
|
|
212
|
+
}
|
|
213
|
+
interface Scenario {
|
|
214
|
+
id: string;
|
|
215
|
+
name: string;
|
|
216
|
+
description: string;
|
|
217
|
+
owner: string;
|
|
218
|
+
createdAt: string;
|
|
219
|
+
changes: ScenarioChange[];
|
|
220
|
+
changeLog: ChangeLogEntry[];
|
|
221
|
+
commentary: string;
|
|
222
|
+
}
|
|
223
|
+
interface SpanBucket {
|
|
224
|
+
label: string;
|
|
225
|
+
min: number;
|
|
226
|
+
max: number;
|
|
227
|
+
count: number;
|
|
228
|
+
managerIds: string[];
|
|
229
|
+
}
|
|
230
|
+
interface LayerStat {
|
|
231
|
+
layer: number;
|
|
232
|
+
headcount: number;
|
|
233
|
+
managers: number;
|
|
234
|
+
ics: number;
|
|
235
|
+
cost: number;
|
|
236
|
+
managementCost: number;
|
|
237
|
+
avgSpan: number;
|
|
238
|
+
medianSpan: number;
|
|
239
|
+
unfilledSeats: number;
|
|
240
|
+
}
|
|
241
|
+
interface OrgMetrics {
|
|
242
|
+
headcount: number;
|
|
243
|
+
totalFte: number;
|
|
244
|
+
totalCost: number;
|
|
245
|
+
filledHeadcount: number;
|
|
246
|
+
vacantSeats: number;
|
|
247
|
+
tbhSeats: number;
|
|
248
|
+
unfilledSeats: number;
|
|
249
|
+
vacancyRate: number;
|
|
250
|
+
runRateCost: number;
|
|
251
|
+
unfilledSeatCost: number;
|
|
252
|
+
avgFilledSpan: number;
|
|
253
|
+
medianFilledSpan: number;
|
|
254
|
+
unfilledByFunction: Record<string, number>;
|
|
255
|
+
unfilledByLayer: Record<number, number>;
|
|
256
|
+
managerCount: number;
|
|
257
|
+
icCount: number;
|
|
258
|
+
managerRatio: number;
|
|
259
|
+
managementCost: number;
|
|
260
|
+
icCost: number;
|
|
261
|
+
managementCostRatio: number;
|
|
262
|
+
maxLayers: number;
|
|
263
|
+
avgSpan: number;
|
|
264
|
+
medianSpan: number;
|
|
265
|
+
narrowSpanManagers: string[];
|
|
266
|
+
wideSpanManagers: string[];
|
|
267
|
+
narrowSpanPct: number;
|
|
268
|
+
wideSpanPct: number;
|
|
269
|
+
narrowSpanCost: number;
|
|
270
|
+
spanBuckets: SpanBucket[];
|
|
271
|
+
layerStats: LayerStat[];
|
|
272
|
+
costByFunction: Record<string, number>;
|
|
273
|
+
headcountByFunction: Record<string, number>;
|
|
274
|
+
layersByFunction: Record<string, number>;
|
|
275
|
+
avgSpanByFunction: Record<string, number>;
|
|
276
|
+
costByBU: Record<string, number>;
|
|
277
|
+
headcountByBU: Record<string, number>;
|
|
278
|
+
layersByBU: Record<string, number>;
|
|
279
|
+
costByLocation: Record<string, number>;
|
|
280
|
+
headcountByLocation: Record<string, number>;
|
|
281
|
+
costByGrade: Record<string, number>;
|
|
282
|
+
headcountByGrade: Record<string, number>;
|
|
283
|
+
costByLayer: Record<number, number>;
|
|
284
|
+
managementCostByLayer: Record<number, number>;
|
|
285
|
+
layerTaxPerIC: number;
|
|
286
|
+
managerOfOneCount: number;
|
|
287
|
+
managerOfManagersCount: number;
|
|
288
|
+
pureSupervisoryLayers: number[];
|
|
289
|
+
aiExposure: {
|
|
290
|
+
band: "high" | "medium" | "low";
|
|
291
|
+
headcount: number;
|
|
292
|
+
cost: number;
|
|
293
|
+
}[];
|
|
294
|
+
duplicateRoles: DuplicateRoleGroup[];
|
|
295
|
+
duplicateRoleCost: number;
|
|
296
|
+
deepestChains: {
|
|
297
|
+
ids: string[];
|
|
298
|
+
depth: number;
|
|
299
|
+
}[];
|
|
300
|
+
delayeringOpportunity: number;
|
|
301
|
+
spanOptimizationOpportunity: number;
|
|
302
|
+
duplicationOpportunity: number;
|
|
303
|
+
totalOpportunity: number;
|
|
304
|
+
orgHealth: OrgHealthScore;
|
|
305
|
+
}
|
|
306
|
+
interface DuplicateRoleGroup {
|
|
307
|
+
normalizedTitle: string;
|
|
308
|
+
employees: {
|
|
309
|
+
id: string;
|
|
310
|
+
name: string;
|
|
311
|
+
title: string;
|
|
312
|
+
unit: string;
|
|
313
|
+
cost: number;
|
|
314
|
+
}[];
|
|
315
|
+
units: string[];
|
|
316
|
+
totalCost: number;
|
|
317
|
+
/** Distinct raw titles that normalized into this group — lets the UI show the normalization. */
|
|
318
|
+
normalizedFrom: string[];
|
|
319
|
+
}
|
|
320
|
+
interface OrgHealthScore {
|
|
321
|
+
total: number;
|
|
322
|
+
components: {
|
|
323
|
+
name: string;
|
|
324
|
+
score: number;
|
|
325
|
+
weight: number;
|
|
326
|
+
explanation: string;
|
|
327
|
+
}[];
|
|
328
|
+
}
|
|
329
|
+
type InsightCategory = "structure" | "cost" | "span" | "layer" | "duplication" | "location" | "grade" | "risk" | "scenario" | "executive";
|
|
330
|
+
interface Insight {
|
|
331
|
+
id: string;
|
|
332
|
+
title: string;
|
|
333
|
+
severity: "high" | "medium" | "low";
|
|
334
|
+
category: InsightCategory;
|
|
335
|
+
evidence: string;
|
|
336
|
+
impact: string;
|
|
337
|
+
recommendation: string;
|
|
338
|
+
relatedEntities: string[];
|
|
339
|
+
financialImpact: number;
|
|
340
|
+
employeesAffected: number;
|
|
341
|
+
confidence: "high" | "medium" | "low";
|
|
342
|
+
easeOfAction: "easy" | "moderate" | "hard";
|
|
343
|
+
linkTo: string;
|
|
344
|
+
rank?: number;
|
|
345
|
+
}
|
|
346
|
+
interface ChangeRisk {
|
|
347
|
+
level: "Low" | "Medium" | "High" | "Very High";
|
|
348
|
+
score: number;
|
|
349
|
+
factors: {
|
|
350
|
+
name: string;
|
|
351
|
+
value: number | string;
|
|
352
|
+
contribution: number;
|
|
353
|
+
}[];
|
|
354
|
+
recommendation: string;
|
|
355
|
+
}
|
|
356
|
+
interface ScenarioScore {
|
|
357
|
+
total: number;
|
|
358
|
+
categories: {
|
|
359
|
+
name: string;
|
|
360
|
+
score: number;
|
|
361
|
+
weight: number;
|
|
362
|
+
}[];
|
|
363
|
+
}
|
|
364
|
+
interface ScenarioEvaluation {
|
|
365
|
+
scenarioId: string;
|
|
366
|
+
metrics: OrgMetrics;
|
|
367
|
+
risk: ChangeRisk;
|
|
368
|
+
score: ScenarioScore;
|
|
369
|
+
costDelta: number;
|
|
370
|
+
headcountDelta: number;
|
|
371
|
+
layerDelta: number;
|
|
372
|
+
employeesMoved: number;
|
|
373
|
+
rolesRemoved: number;
|
|
374
|
+
rolesAdded: number;
|
|
375
|
+
transitionCost: number;
|
|
376
|
+
netYear1Saving: number;
|
|
377
|
+
runRateSaving: number;
|
|
378
|
+
year1CapturePct: number;
|
|
379
|
+
vacantSeatsRemoved: number;
|
|
380
|
+
seatsFilled: number;
|
|
381
|
+
}
|
|
382
|
+
declare const REQUIRED_FIELDS: readonly ["employee_id", "employee_name", "role_title", "manager_id", "department", "function", "business_unit", "location", "country", "grade", "level", "employment_type", "fte", "salary", "bonus", "total_cost", "status"];
|
|
383
|
+
declare const OPTIONAL_FIELDS: readonly ["manager_name", "position_status", "tenure", "gender", "age_band", "performance_rating", "potential_rating", "critical_role", "role_family", "role_type", "cost_center", "legal_entity", "contract_type", "remote_status", "skills", "succession_risk", "attrition_risk", "change_impact", "comments"];
|
|
384
|
+
type FieldMapping = Record<string, string | null>;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Hierarchy engine: builds the org tree from flat employee records,
|
|
388
|
+
* computes layers, spans, downstream rollups, and detects structural defects
|
|
389
|
+
* (cycles, orphans, multiple roots).
|
|
390
|
+
*
|
|
391
|
+
* All functions are pure — they never mutate the input employee array.
|
|
392
|
+
*/
|
|
393
|
+
|
|
394
|
+
declare function buildOrgTree(employees: Employee[]): OrgTree;
|
|
395
|
+
/** All employee ids in the subtree rooted at `id` (including `id`). */
|
|
396
|
+
declare function subtreeIds(tree: OrgTree, id: string): string[];
|
|
397
|
+
/** Longest reporting chains (root → leaf), top N. */
|
|
398
|
+
declare function deepestChains(tree: OrgTree, topN?: number): {
|
|
399
|
+
ids: string[];
|
|
400
|
+
depth: number;
|
|
401
|
+
}[];
|
|
402
|
+
/** Would moving `employeeId` under `newManagerId` create a cycle? */
|
|
403
|
+
declare function wouldCreateCycle(tree: OrgTree, employeeId: string, newManagerId: string): boolean;
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Normalize a role title for duplication grouping. Noise words default to
|
|
407
|
+
* DEFAULT_TITLE_NOISE_WORDS (seniority markers + roman/arabic numerals) and are
|
|
408
|
+
* configurable via assumptions.titleNoiseWords. Words like "assistant", "lead"
|
|
409
|
+
* and "associate" are deliberately NOT noise by default — they usually denote
|
|
410
|
+
* genuinely different jobs.
|
|
411
|
+
*/
|
|
412
|
+
declare function normalizeTitle(title: string, noiseWords?: string[]): string;
|
|
413
|
+
/**
|
|
414
|
+
* Resolve the applicable target span band for an employee level from
|
|
415
|
+
* assumptions.targetSpansByLevel (assumed sorted by level ascending). Picks the
|
|
416
|
+
* entry with the largest level <= the employee level; if the level is below the
|
|
417
|
+
* first entry's level, the first entry is used. Returns undefined if there are
|
|
418
|
+
* no bands configured.
|
|
419
|
+
*/
|
|
420
|
+
declare function spanBandForLevel(level: number, assumptions: Assumptions): SpanTargetByLevel | undefined;
|
|
421
|
+
/**
|
|
422
|
+
* Level-aware narrow/wide classification for a single manager span.
|
|
423
|
+
* When the employee level is 0/blank, falls back to the flat thresholds.
|
|
424
|
+
*/
|
|
425
|
+
declare function classifySpan(span: number, level: number, assumptions: Assumptions): {
|
|
426
|
+
narrow: boolean;
|
|
427
|
+
wide: boolean;
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Classify a role's AI-augmentation potential. Uses an explicit role_family
|
|
431
|
+
* mapping when available; otherwise falls back to a conservative title-keyword
|
|
432
|
+
* heuristic. Augmentation potential is NOT a judgement on elimination.
|
|
433
|
+
*/
|
|
434
|
+
declare function classifyAiAugmentation(employee: Employee, assumptions: Assumptions): "high" | "medium" | "low";
|
|
435
|
+
/**
|
|
436
|
+
* Potential duplicate leadership/specialist roles: same normalized title held
|
|
437
|
+
* in 2+ different business units (or 3+ holders in one unit) among the senior
|
|
438
|
+
* population. Seniority = explicit level 1–4; employees with no level data
|
|
439
|
+
* (level 0/blank) use a cost-percentile fallback — only the top 25% of
|
|
440
|
+
* total_cost within the dataset counts as senior, so unleveled populations are
|
|
441
|
+
* never flagged wholesale. Flagged as "potential duplication requiring
|
|
442
|
+
* review" — never auto-judged bad.
|
|
443
|
+
*/
|
|
444
|
+
declare function findDuplicateRoles(employees: Employee[], assumptions?: Assumptions): DuplicateRoleGroup[];
|
|
445
|
+
declare function computeMetrics(employees: Employee[], assumptions: Assumptions): OrgMetrics;
|
|
446
|
+
interface HealthInputs {
|
|
447
|
+
maxLayers: number;
|
|
448
|
+
headcount: number;
|
|
449
|
+
narrowPct: number;
|
|
450
|
+
widePct: number;
|
|
451
|
+
managerRatio: number;
|
|
452
|
+
mgmtCostRatio: number;
|
|
453
|
+
duplicateCost: number;
|
|
454
|
+
totalCost: number;
|
|
455
|
+
}
|
|
456
|
+
declare function computeOrgHealth(inp: HealthInputs, assumptions: Assumptions): OrgHealthScore;
|
|
457
|
+
|
|
458
|
+
declare function validateEmployees(employees: Employee[]): DataQualityReport;
|
|
459
|
+
/**
|
|
460
|
+
* Normalize a raw position-status cell to a PositionStatus. Recognizes common
|
|
461
|
+
* HRIS spellings case-/punctuation-insensitively; unknown values return
|
|
462
|
+
* undefined so callers can fall back to other signals (the safe default for
|
|
463
|
+
* anything unrecognized is "filled").
|
|
464
|
+
*/
|
|
465
|
+
declare function normalizePositionStatus(v: unknown): PositionStatus | undefined;
|
|
466
|
+
declare function rowsToEmployees(rows: Record<string, unknown>[], mapping: Record<string, string | null>): Employee[];
|
|
467
|
+
/**
|
|
468
|
+
* Auto-map uploaded column names to canonical fields by fuzzy name match.
|
|
469
|
+
*
|
|
470
|
+
* `extraAliases` (e.g. from an HRIS import template) are merged with the
|
|
471
|
+
* built-in ALIASES using the same normalization; where both could match,
|
|
472
|
+
* the extra aliases take precedence — exact extra match, then exact
|
|
473
|
+
* built-in, then fuzzy extra, then fuzzy built-in.
|
|
474
|
+
*/
|
|
475
|
+
declare function autoMapColumns(columns: string[], fields: readonly string[], extraAliases?: Record<string, string[]>): Record<string, string | null>;
|
|
476
|
+
|
|
477
|
+
interface ApplyResult {
|
|
478
|
+
employees: Employee[];
|
|
479
|
+
warnings: string[];
|
|
480
|
+
}
|
|
481
|
+
/** Replay a change list onto the baseline. Pure — returns a new array. */
|
|
482
|
+
declare function applyChanges(baseline: Employee[], changes: ScenarioChange[]): ApplyResult;
|
|
483
|
+
/** Employees whose manager changed or who were removed/added vs baseline. */
|
|
484
|
+
declare function diffEmployees(baseline: Employee[], scenario: Employee[]): {
|
|
485
|
+
moved: Employee[];
|
|
486
|
+
removed: Employee[];
|
|
487
|
+
added: Employee[];
|
|
488
|
+
costChanged: Employee[];
|
|
489
|
+
locationChanged: Employee[];
|
|
490
|
+
seatsFilled: Employee[];
|
|
491
|
+
};
|
|
492
|
+
declare function computeChangeRisk(baseline: Employee[], scenarioEmployees: Employee[], baselineMetrics: OrgMetrics, scenarioMetrics: OrgMetrics): ChangeRisk;
|
|
493
|
+
/**
|
|
494
|
+
* Share (0–1) of the run-rate saving captured in year 1, derived from the
|
|
495
|
+
* month the changes take effect: (12 − effectiveMonth + 1)/12. Month 1 →
|
|
496
|
+
* full-year capture; month 12 → 1/12. Guards stale persisted assumptions
|
|
497
|
+
* (missing/out-of-range months fall back to the default).
|
|
498
|
+
*/
|
|
499
|
+
declare function year1CapturePct(assumptions: Assumptions): number;
|
|
500
|
+
/**
|
|
501
|
+
* One-time + ramp cost of executing a scenario, computed additively:
|
|
502
|
+
* - severance: monthly cost (total_cost/12) × severance months, per removed role
|
|
503
|
+
* (severance months are scaled by the employee country's severance multiplier)
|
|
504
|
+
* - backfill drag: backfillPct% of removed-role cost (annualized hiring/ramp)
|
|
505
|
+
* - change cost: oneTimeChangeCostPerMove × employees moved
|
|
506
|
+
* - location shifts: treated conservatively as exit-and-rehire — severance on
|
|
507
|
+
* the BASELINE cost + backfillPct% of baseline cost (hiring/ramp in the new
|
|
508
|
+
* location) + oneTimeChangeCostPerMove, per location-changed employee
|
|
509
|
+
* - cost changes without a location change: oneTimeChangeCostPerMove only
|
|
510
|
+
* - seat fills: backfillPct% of the seat's budgeted cost (hiring/ramp drag)
|
|
511
|
+
*
|
|
512
|
+
* Position-vs-person: vacant/TBH seats have nobody in them, so changes to
|
|
513
|
+
* unfilled seats move budget, not people — eliminating, moving or re-locating
|
|
514
|
+
* an unfilled seat incurs NO severance, backfill or per-move cost.
|
|
515
|
+
*/
|
|
516
|
+
declare function computeTransitionCost(baseline: Employee[], scenarioEmployees: Employee[], assumptions: Assumptions): number;
|
|
517
|
+
declare function scoreScenario(baselineMetrics: OrgMetrics, scenarioMetrics: OrgMetrics, risk: ChangeRisk, assumptions: Assumptions, changeCount: number, netYear1Saving?: number, affectedEmployees?: number): ScenarioScore;
|
|
518
|
+
declare function evaluateScenario(baseline: Employee[], scenario: Scenario, baselineMetrics: OrgMetrics, assumptions: Assumptions): ScenarioEvaluation;
|
|
519
|
+
interface SuggestedAction {
|
|
520
|
+
change: ScenarioChange;
|
|
521
|
+
rationale: string;
|
|
522
|
+
estimatedSaving: number;
|
|
523
|
+
}
|
|
524
|
+
/** Rule-based consolidation suggestions: merge narrow-span managers into peers. */
|
|
525
|
+
declare function suggestConsolidations(employees: Employee[], assumptions: Assumptions, scopeFunction?: string): SuggestedAction[];
|
|
526
|
+
|
|
527
|
+
interface InsightContext {
|
|
528
|
+
employees: Employee[];
|
|
529
|
+
metrics: OrgMetrics;
|
|
530
|
+
assumptions: Assumptions;
|
|
531
|
+
scenarioEvaluations?: {
|
|
532
|
+
name: string;
|
|
533
|
+
evaluation: ScenarioEvaluation;
|
|
534
|
+
}[];
|
|
535
|
+
}
|
|
536
|
+
declare function generateInsights(ctx: InsightContext): Insight[];
|
|
537
|
+
/** Rank: severity, then financial impact, then employees affected, confidence, ease. */
|
|
538
|
+
declare function rankInsights(insights: Insight[]): Insight[];
|
|
539
|
+
/** Convenience: compute everything from raw employees. */
|
|
540
|
+
declare function generateInsightsFromEmployees(employees: Employee[], assumptions: Assumptions, scenarioEvaluations?: {
|
|
541
|
+
name: string;
|
|
542
|
+
evaluation: ScenarioEvaluation;
|
|
543
|
+
}[]): Insight[];
|
|
544
|
+
|
|
545
|
+
interface LayoutDatum {
|
|
546
|
+
id: string;
|
|
547
|
+
node: EmployeeNode;
|
|
548
|
+
collapsed: boolean;
|
|
549
|
+
children?: LayoutDatum[];
|
|
550
|
+
}
|
|
551
|
+
interface PositionedNode {
|
|
552
|
+
id: string;
|
|
553
|
+
x: number;
|
|
554
|
+
y: number;
|
|
555
|
+
node: EmployeeNode;
|
|
556
|
+
collapsed: boolean;
|
|
557
|
+
}
|
|
558
|
+
interface PositionedEdge {
|
|
559
|
+
id: string;
|
|
560
|
+
/** Elbow path string in canvas coordinates. */
|
|
561
|
+
path: string;
|
|
562
|
+
sourceId: string;
|
|
563
|
+
targetId: string;
|
|
564
|
+
}
|
|
565
|
+
interface CanvasLayout {
|
|
566
|
+
nodes: PositionedNode[];
|
|
567
|
+
edges: PositionedEdge[];
|
|
568
|
+
bounds: {
|
|
569
|
+
minX: number;
|
|
570
|
+
minY: number;
|
|
571
|
+
maxX: number;
|
|
572
|
+
maxY: number;
|
|
573
|
+
};
|
|
574
|
+
visibleCount: number;
|
|
575
|
+
}
|
|
576
|
+
interface LayoutOptions {
|
|
577
|
+
nodeWidth: number;
|
|
578
|
+
nodeHeight: number;
|
|
579
|
+
gapX: number;
|
|
580
|
+
gapY: number;
|
|
581
|
+
}
|
|
582
|
+
declare const FULL_NODE: LayoutOptions;
|
|
583
|
+
declare const COMPACT_NODE: LayoutOptions;
|
|
584
|
+
/** Run tidy-tree layout; returns positioned nodes/edges in canvas coords. */
|
|
585
|
+
declare function layoutOrg(orgTree: OrgTree, rootIds: string[], isExpanded: (id: string) => boolean, opts: LayoutOptions): CanvasLayout;
|
|
586
|
+
/** Orthogonal elbow: parent bottom-center → child top-center, rounded corners. */
|
|
587
|
+
declare function elbowPath(x1: number, y1: number, x2: number, y2: number): string;
|
|
588
|
+
/** Viewport transform helpers (pan/zoom math, kept pure for tests). */
|
|
589
|
+
interface Viewport {
|
|
590
|
+
x: number;
|
|
591
|
+
y: number;
|
|
592
|
+
k: number;
|
|
593
|
+
}
|
|
594
|
+
declare function fitToBounds(bounds: CanvasLayout["bounds"], containerW: number, containerH: number, padding?: number, maxK?: number, minK?: number): Viewport;
|
|
595
|
+
/** Zoom around a screen point (cursor), clamped. */
|
|
596
|
+
declare function zoomAround(vp: Viewport, screenX: number, screenY: number, factor: number, minK?: number, maxK?: number): Viewport;
|
|
597
|
+
/** Nodes intersecting the visible canvas region (viewport culling). */
|
|
598
|
+
declare function visibleNodes(nodes: PositionedNode[], vp: Viewport, containerW: number, containerH: number, opts: LayoutOptions, overscan?: number): PositionedNode[];
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Demo dataset: "Meridian Global Services" — a fictional ~300-person company.
|
|
602
|
+
*
|
|
603
|
+
* Deterministic (seeded PRNG) so every load produces the same organization.
|
|
604
|
+
* The structure deliberately contains consulting-style findings:
|
|
605
|
+
* - Technology is over-layered (8 layers) with a deep infrastructure chain
|
|
606
|
+
* - Several narrow-span managers (1–2 reports), esp. in Finance & Technology
|
|
607
|
+
* - Two overloaded managers (14–16 reports) in Customer Service & Operations
|
|
608
|
+
* - Duplicate senior roles across business units (Heads of PMO, Ops Excellence)
|
|
609
|
+
* - Expensive senior ICs reporting directly to executives
|
|
610
|
+
* - Inconsistent role-title spellings
|
|
611
|
+
* - One employee with an invalid manager reference, one zero-cost row
|
|
612
|
+
*/
|
|
613
|
+
|
|
614
|
+
declare function generateDemoData(): Employee[];
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Built-in HRIS import templates: column-name aliases + export instructions
|
|
618
|
+
* for the most common HR systems. Selecting a template on the upload page
|
|
619
|
+
* seeds `autoMapColumns` with each system's standard report/export field
|
|
620
|
+
* names so a raw export maps onto the canonical model with little manual work.
|
|
621
|
+
*
|
|
622
|
+
* Aliases are matched with the same normalization as autoMapColumns
|
|
623
|
+
* (lowercase, alphanumerics only), so spelling variants like "Reports To"
|
|
624
|
+
* vs "ReportsTo" need only one entry.
|
|
625
|
+
*/
|
|
626
|
+
interface HrisTemplate {
|
|
627
|
+
/** Stable identifier, e.g. "workday". */
|
|
628
|
+
id: string;
|
|
629
|
+
/** Display name, e.g. "Workday". */
|
|
630
|
+
name: string;
|
|
631
|
+
/** Canonical field -> column-name aliases as they appear in this system's exports. */
|
|
632
|
+
aliases: Record<string, string[]>;
|
|
633
|
+
/** Numbered steps an admin follows to pull the right report from the system. */
|
|
634
|
+
exportInstructions: string[];
|
|
635
|
+
/** Optional caveats about the system's export format. */
|
|
636
|
+
notes?: string;
|
|
637
|
+
}
|
|
638
|
+
declare const HRIS_TEMPLATES: HrisTemplate[];
|
|
639
|
+
/** Look up a template by its stable id. */
|
|
640
|
+
declare function getHrisTemplate(id: string): HrisTemplate | undefined;
|
|
641
|
+
|
|
642
|
+
export { type ApplyResult, type Assumptions, COMPACT_NODE, type CanvasLayout, type ChangeLogEntry, type ChangeRisk, DEFAULT_ASSUMPTIONS, DEFAULT_TITLE_NOISE_WORDS, type DataQualityIssue, type DataQualityReport, type DuplicateRoleGroup, type Employee, type EmployeeNode, FULL_NODE, type FieldMapping, HRIS_TEMPLATES, type HrisTemplate, type Insight, type InsightCategory, type InsightContext, type IssueSeverity, type LayerStat, type LayoutDatum, type LayoutOptions, MONTH_NAMES, OPTIONAL_FIELDS, type OrgHealthScore, type OrgMetrics, type OrgTree, type PositionStatus, type PositionedEdge, type PositionedNode, REQUIRED_FIELDS, type Scenario, type ScenarioChange, type ScenarioEvaluation, type ScenarioScore, type ScenarioScoringWeights, type SpanBucket, type SpanTargetByLevel, type SuggestedAction, type TransitionAssumptions, type Viewport, applyChanges, autoMapColumns, buildOrgTree, classifyAiAugmentation, classifySpan, computeChangeRisk, computeMetrics, computeOrgHealth, computeTransitionCost, deepestChains, diffEmployees, elbowPath, evaluateScenario, findDuplicateRoles, fitToBounds, generateDemoData, generateInsights, generateInsightsFromEmployees, getHrisTemplate, layoutOrg, normalizePositionStatus, normalizeTitle, positionStatus, rankInsights, rowsToEmployees, scoreScenario, spanBandForLevel, subtreeIds, suggestConsolidations, validateEmployees, visibleNodes, wouldCreateCycle, year1CapturePct, zoomAround };
|