@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.
@@ -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 };