@outbuild-company/schedule-core 1.0.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,2122 @@
1
+ /**
2
+ * Shared types for the custom AutoScheduler and its satellite packages
3
+ * (recording, replay, shadow, discovery).
4
+ *
5
+ * This file is the single seam between all components. All scheduler code
6
+ * depends ONLY on these types — never on the DHTMLX gantt object directly.
7
+ */
8
+ type ActivityId$2 = string;
9
+ type LinkId$1 = string;
10
+ type CalendarId$2 = string;
11
+
12
+ /** Value constants for link types. Matches `gantt.config.links`. */
13
+ declare const LINK_TYPE: {
14
+ readonly FINISH_TO_START: "0";
15
+ readonly START_TO_START: "1";
16
+ readonly FINISH_TO_FINISH: "2";
17
+ readonly START_TO_FINISH: "3";
18
+ };
19
+ /**
20
+ * RAW DHTMLX row as the scheduler engine and `ScheduleState` see it:
21
+ * snake_case fields, `parent` with the numeric-0 root sentinel,
22
+ * `$source`/`$target` link id arrays, `duration` in HOURS, ids
23
+ * `string | number`. Dates are real Date objects (not ISO strings);
24
+ * serialization for JSON fixtures happens in the recording layer.
25
+ * Sibling shapes: persisted `Activity` (`src/types.ts`) and the
26
+ * pipelines' camelCase `ActivitySnapshot` (`src/columns/types.ts`) —
27
+ * see the `Activity` docblock for the full map.
28
+ */
29
+ interface EngineActivitySnapshot {
30
+ id: ActivityId$2;
31
+ start_date: Date;
32
+ end_date: Date;
33
+ duration: number;
34
+ type: ActivityType;
35
+ parent: ActivityId$2 | 0;
36
+ constraint_type: ConstraintType | null;
37
+ constraint_date: Date | null;
38
+ progress: number;
39
+ auto_scheduling: boolean;
40
+ calendar_id: CalendarId$2 | null;
41
+ /** Outgoing link ids (this activity is the link source) */
42
+ $source: LinkId$1[];
43
+ /** Incoming link ids (this activity is the link target) */
44
+ $target: LinkId$1[];
45
+ /** Mirrors `Activity.is_critical` (`CriticalFlag`): boolean fresh from the
46
+ * backend, `'Si'`/`'No'` after the critical-path pass. */
47
+ is_critical?: boolean | 'Si' | 'No';
48
+ dragged?: boolean;
49
+ /**
50
+ * Display/domain fields carried at runtime via the activityToSnapshot `...a`
51
+ * spread; declared so state-ports reads them typed instead of casting.
52
+ */
53
+ text?: string;
54
+ description?: string;
55
+ cost?: number;
56
+ used_cost?: number;
57
+ real_cost?: number;
58
+ hhWorkTime?: number;
59
+ real_work?: number;
60
+ ponderator?: number;
61
+ tasks?: unknown[];
62
+ real_constraint_type?: ConstraintType | null;
63
+ last_constraint?: ConstraintType | null;
64
+ last_constraint_date?: Date | null;
65
+ last_start_date?: Date | null;
66
+ date_origin?: Date | null;
67
+ /**
68
+ * Backend DB row id (distinct from `id` which is `unique_id`). Optional
69
+ * because activities created in-session have no backend row yet.
70
+ */
71
+ proplannerId?: number;
72
+ /** Sequential display index used as a short human-readable activity label.
73
+ * `| undefined` is explicit: rollback before-images assign the prior value,
74
+ * which is `undefined` for an activity that had no correlative_id yet. */
75
+ correlative_id?: number | undefined;
76
+ /**
77
+ * Project-scoped unique identifier string, user-visible (e.g. "A-0001").
78
+ * Null for activities that have not been assigned one yet.
79
+ */
80
+ custom_id?: string | null;
81
+ /**
82
+ * UUID-like correlative assigned by Outbuild. May be string or number
83
+ * depending on the backend version; absent on in-session-created activities.
84
+ */
85
+ unique_correlative_id?: string | number | null;
86
+ /**
87
+ * Working-hours mirror of `duration` used by the Duration column renderer to
88
+ * avoid re-computing hours from days on every render. Stamped by
89
+ * `parseFromBackend`, `activityCreationPipeline`, and parent-bounds.
90
+ */
91
+ for_disable_milestone_duration?: number;
92
+ /**
93
+ * View-model visibility bit (UI filters); ex `should_be_showed` of the
94
+ * DHTMLX task. `visible !== false` = visible; default true.
95
+ */
96
+ visible?: boolean;
97
+ /** Checkbox-selection bit owned by the selection pipeline. */
98
+ checked?: boolean;
99
+ /** Visible (UI-layer) checkbox state, kept in sync with `checked`. */
100
+ visibleChecked?: boolean;
101
+ /** Signal to the bridge that `visibleChecked` was set this dispatch. */
102
+ mustApplyVisibleChecked?: boolean;
103
+ /**
104
+ * Pending Schedule Impact Requests freezing this activity (id-only
105
+ * reduction; non-empty ⇒ frozen). Carried verbatim from the backend payload.
106
+ * Only for dynamic setActivityField writes + arbitrary backend passthrough;
107
+ * all known fields are declared above.
108
+ */
109
+ pendingRequests?: PendingRequest[];
110
+ earlyStart?: Date | null;
111
+ earlyFinish?: Date | null;
112
+ lateStart?: Date | null;
113
+ lateFinish?: Date | null;
114
+ totalSlack?: number | null;
115
+ freeSlack?: number | null;
116
+ /** Expected-progress baseline output, written by the expected-progress pass. */
117
+ expected_progress?: number;
118
+ /** Lookahead membership; the core derives it in promote/demote (R4). */
119
+ is_lookahead?: boolean;
120
+ /** Client-side "new, unsaved" marker (not persisted as such). */
121
+ isNewActivity?: true | null;
122
+ /** Descendant new-activity id bookkeeping (R15, temporal). */
123
+ newActivitiesArray?: ActivityId$2[];
124
+ /** `newActivitiesArray.length > 0` mirror (R15, temporal). */
125
+ hasNewActivities?: boolean;
126
+ /** Activity modification log carried from the backend. */
127
+ activityModifications?: unknown[];
128
+ /** Load-derived: the activity fell back to the default calendar. */
129
+ assignedDefaultCalendar?: boolean;
130
+ /** Backend flag: has persisted children. */
131
+ has_childs?: boolean;
132
+ /** Backend flag: ponderator set manually (not auto-distributed). */
133
+ hasCustomPonderator?: boolean;
134
+ /** Backend aggregate: recursive duration sum. */
135
+ sumOfDurationRecursively?: number;
136
+ /** Backend row ids (distinct from the domain `id`). */
137
+ ganttId?: number;
138
+ sectorId?: number;
139
+ companyId?: number;
140
+ /** Subcontract column id + its raw backend object. */
141
+ subcontractId?: number | null;
142
+ subcontract?: unknown;
143
+ /** Responsibility / tag columns (arrays of backend rows). */
144
+ responsables?: unknown[];
145
+ tags?: unknown[];
146
+ /** Active baseline points overlaid at load. */
147
+ baseline_points?: BaselinePoint[];
148
+ /** Backend timestamps. */
149
+ createdAt?: string;
150
+ updatedAt?: string;
151
+ old_progress_registered?: number;
152
+ old_duration_registered?: number;
153
+ old_cost_registered?: number;
154
+ old_used_cost_registered?: number;
155
+ old_real_cost_registered?: number;
156
+ old_hhWorkTime_registered?: number;
157
+ /** Mirror of the constraint before the last change (constraint cluster). */
158
+ constraint_type_old?: ConstraintType | null;
159
+ /** `type === 'milestone'` cache flipped by the duration pipeline. */
160
+ is_milestone?: boolean;
161
+ /** DHTMLX markers consumed by the bridge / render layer. */
162
+ $keep_constraints?: boolean;
163
+ $rendered_type?: ActivityType;
164
+ $no_start?: boolean;
165
+ $no_end?: boolean;
166
+ status?: 'Waiting' | 'Doing' | 'Done';
167
+ progressSolidColor?: string;
168
+ color?: string;
169
+ /** Flags a recursive complete/uncomplete from the action-button flow. */
170
+ progressChangedByActionButton?: boolean;
171
+ custom_predecessors?: string;
172
+ /** Backend typo preserved (single 'c'). */
173
+ custom_sucessors?: string;
174
+ start_base?: Date;
175
+ end_base?: Date;
176
+ duration_base?: number;
177
+ cost_base?: number;
178
+ work_base?: number;
179
+ expected_progress_base?: number;
180
+ }
181
+ interface LinkSnapshot {
182
+ id: LinkId$1;
183
+ source: ActivityId$2;
184
+ target: ActivityId$2;
185
+ type: LinkType;
186
+ lag: number;
187
+ /**
188
+ * Precomputed lag components for a VIRTUAL summary-expanded link. When
189
+ * present, the link calculator uses them verbatim instead of deriving the
190
+ * components from leaf durations (the normal `normalize` switch).
191
+ *
192
+ * These exist so an FF/SF link INTO a summary can carry the SUMMARY's
193
+ * duration (`_targetLag = -summary.duration`) and the per-leaf offset from
194
+ * the summary's start (`_trueLag = userLag + off_tgt(leaf)`), matching legacy
195
+ * `_getImplicitLinks`/`_convertToFinishToStartLink` under
196
+ * `auto_scheduling_move_projects`. Computed ONCE at expansion time
197
+ * (`parent-link-expansion.ts`), never per scheduling tick.
198
+ *
199
+ * All three must be set together (or none): a half-populated link would mix
200
+ * precomputed and derived components. Only the FF/SF-into-summary path emits
201
+ * them; every other link leaves them undefined and hits the fast path.
202
+ */
203
+ _sourceLag?: number;
204
+ _targetLag?: number;
205
+ _trueLag?: number;
206
+ ganttId?: number;
207
+ sectorId?: number;
208
+ /** BE row id (distinct from the schedule-level `id`). */
209
+ proplannerId?: number;
210
+ }
211
+
212
+ /**
213
+ * Constraint Rules — single source of truth.
214
+ *
215
+ * This module owns the definition of constraint types and the rules that
216
+ * govern them. Both the autoscheduler and the manual scheduling pipelines
217
+ * (column edits, drag, bulk) consume the same primitives from here.
218
+ *
219
+ * Two surfaces:
220
+ *
221
+ * (a) Manual scheduling — pipelines call `checkConstraintViolation()` after
222
+ * a user types a new value, to decide whether to emit a warning.
223
+ * Surface: scalar Date inputs, no adapter needed.
224
+ *
225
+ * (b) Autoscheduler — `computeConstraintBounds()` derives the four date
226
+ * boundaries (earliest/latest × start/end) an activity's constraint
227
+ * imposes. `isStartWithinBounds()` and `isEndWithinBounds()` are used
228
+ * by `limitPlanDates()` to decide whether a candidate date (from a
229
+ * link) respects the constraint.
230
+ *
231
+ * Domain invariant the autoscheduler must respect:
232
+ * The autoscheduler never moves an activity to a date that violates that
233
+ * activity's own constraint. If a cascade via a link would push the
234
+ * activity outside its constraint bounds, the activity stays put.
235
+ *
236
+ * Manual scheduling does NOT enforce this — a user is allowed to type a
237
+ * value that violates their own constraint. The pipeline applies the edit
238
+ * and emits a warning; the UI decides what to do with the warning (typically
239
+ * shows a "constraint violation detected" modal and offers to revert).
240
+ */
241
+
242
+ type ConstraintType = 'asap' | 'alap' | 'snet' | 'snlt' | 'fnet' | 'fnlt' | 'mso' | 'mfo';
243
+ declare const CONSTRAINT_TYPE: {
244
+ readonly ASAP: "asap";
245
+ readonly ALAP: "alap";
246
+ readonly SNET: "snet";
247
+ readonly SNLT: "snlt";
248
+ readonly FNET: "fnet";
249
+ readonly FNLT: "fnlt";
250
+ readonly MSO: "mso";
251
+ readonly MFO: "mfo";
252
+ };
253
+
254
+ /**
255
+ * Public internal model types — the normalized form the core works with
256
+ * after parsing the backend payload.
257
+ *
258
+ * Field naming follows snake_case for entity fields (matches backend +
259
+ * DHTMLX convention) and camelCase for everything else, per Q14 in
260
+ * SCHEDULE_CORE_API.md.
261
+ */
262
+ type ActivityId$1 = string;
263
+ type LinkId = string;
264
+ type CalendarId$1 = string;
265
+ type ActivityType = 'project' | 'task' | 'milestone';
266
+
267
+ /** DHTMLX link kinds: 0 = FS, 1 = SS, 2 = FF, 3 = SF. */
268
+ type LinkType = '0' | '1' | '2' | '3';
269
+ /**
270
+ * Critical-path output flag. Carries two historical shapes: `parseFromBackend`
271
+ * stores a boolean, and the critical-path pass (`initial-passes.ts`) overwrites
272
+ * it with the DHTMLX string `'Si'`/`'No'`. Both are observed at runtime (the
273
+ * CP / paste tests assert against `'Si'`/`'No'`; `parseFromBackend` against
274
+ * boolean), so the type unions all three rather than lying with a bare boolean.
275
+ */
276
+ type CriticalFlag = boolean | 'Si' | 'No';
277
+ /** Direction for closest-work-time snapping. */
278
+ type WorkTimeDirection = 'future' | 'past';
279
+ declare const WORK_TIME_DIRECTION: {
280
+ readonly FUTURE: "future";
281
+ readonly PAST: "past";
282
+ };
283
+ /** Calendar query granularity (the engine boundary adds 'minute'/'any'). */
284
+ type CalendarUnit = 'hour' | 'day';
285
+ declare const CALENDAR_UNIT: {
286
+ readonly HOUR: "hour";
287
+ readonly DAY: "day";
288
+ };
289
+ /**
290
+ * Project-level criterion used to redistribute `ponderator` (weight) among
291
+ * sibling activities. Backend stores it lowercase on `projects.activity_creter`
292
+ * (`'duration' | 'cost' | 'hh'`, default `'duration'`); the core normalizes
293
+ * to uppercase. See `getActiveBaseline` / `recomputePonderatorsForParent`.
294
+ */
295
+ type ActivityCreter = 'DURATION' | 'COST' | 'HH';
296
+ /** Criterion for the `status` column: compare progress against baseline or live expected. */
297
+ type StatusCriteria = 'Baseline' | 'Actual';
298
+ /**
299
+ * A versioned container of baseline points for a sector
300
+ * (backend `sectorbaselineversion` model). Exactly one version per sector
301
+ * has `active: true` — that is the one the ponderator computation and the
302
+ * baseline columns read.
303
+ */
304
+ interface BaselineVersion {
305
+ id?: number;
306
+ name?: string | null;
307
+ /** Marks the live baseline. Exactly one active version per sector. */
308
+ active?: boolean;
309
+ visible?: boolean;
310
+ /** Backend extras passed through opaquely. */
311
+ [key: string]: unknown;
312
+ }
313
+ /**
314
+ * A single baseline data point for an activity, as stored by the backend
315
+ * (`sectorbaselinepoint` model) and carried verbatim through the core.
316
+ * Snapshots the activity's planned start/end/duration/cost/work at the
317
+ * moment a baseline version was saved. The schedule reads the ACTIVE point
318
+ * (see `getActiveBaseline`) to derive `ponderator` (baseline-only, no
319
+ * fallback — see memory `project_ponderator_baseline_fix`).
320
+ *
321
+ * Casing note: the frontend baseline columns (`startBaseCol` …
322
+ * `durationBaseCol`) read these fields directly off the activity, so the
323
+ * core preserves the backend snake_case exactly (passthrough, not parsed).
324
+ *
325
+ * Unit trap: `duration` is in DAYS (backend unit), unlike the activity's
326
+ * `duration` which the core models in working hours — convert with the
327
+ * sector's hours-per-day before arithmetic (legacy `transformDaysToHours`).
328
+ */
329
+ interface BaselinePoint {
330
+ /** Planned start, backend date string `"YYYY/MM/DD H:MM"` or ISO. */
331
+ start_date: string;
332
+ /** Planned end (inclusive). */
333
+ end_date: string;
334
+ /** Planned duration in DAYS (backend unit). */
335
+ duration: number;
336
+ /** Planned cost. May arrive as a numeric string. */
337
+ cost: number | string;
338
+ /** Planned work hours (HH). May arrive as a numeric string. */
339
+ hh_work: number | string;
340
+ /** The version this point belongs to; `.active` selects the live one. */
341
+ sectorbaselineversion?: BaselineVersion;
342
+ sectorbaselineversionId?: number;
343
+ baseCalendarId?: number;
344
+ hoursPerDay?: number;
345
+ hoursPerWeek?: number;
346
+ /** Backend extras passed through opaquely. */
347
+ [key: string]: unknown;
348
+ }
349
+ /**
350
+ * Pure-domain activity model produced by `parseFromBackend`. Snake_case
351
+ * matches the backend convention. No index signature — every field is
352
+ * explicit. New backend fields must be added here (or to
353
+ * `ACTIVITY_PASSTHROUGH_FIELDS` in `parseFromBackend.ts` and here).
354
+ *
355
+ * Companion type for runtime mutations: see `ActivityRuntime` below,
356
+ * which extends this with `DhtmlxHeritageFields`.
357
+ */
358
+ /**
359
+ * A pending SIR freezing an activity, reduced to the only field the core
360
+ * needs: its id (equal to the backend SIR id and the front's Redux item id).
361
+ * The schedule load already filters to pending, so presence ⇒ pending.
362
+ */
363
+ interface PendingRequest {
364
+ id: string;
365
+ }
366
+ /**
367
+ * PUBLIC persisted domain model — what `core.getActivityView()` returns and
368
+ * what the backend payload parses into. Fields are snake_case (backend +
369
+ * DHTMLX convention); `duration` is in DAYS at this boundary.
370
+ *
371
+ * The package has three activity shapes — one per layer:
372
+ * - `Activity` (this): public, persisted, snake_case.
373
+ * - `ActivitySnapshot` (`src/columns/types.ts`): camelCase + derived
374
+ * fields (hasChildren, isMilestone…); what column pipelines read.
375
+ * Built per-read by `state-ports.ts`; never persisted.
376
+ * - `EngineActivitySnapshot` (`src/autoscheduler/types.ts`): raw DHTMLX
377
+ * row (snake_case + `parent`/`$source`/`$target`, hours-based
378
+ * duration); what `ScheduleState` stores and the scheduler engine
379
+ * consumes. Translation Activity ↔ engine: `src/dispatch/conversion.ts`.
380
+ */
381
+ interface Activity {
382
+ id: ActivityId$1;
383
+ /** Parent's `id`, or `'0'` when the activity is a root. */
384
+ parent: ActivityId$1 | '0';
385
+ text: string;
386
+ description: string;
387
+ type: ActivityType;
388
+ start_date: Date;
389
+ end_date: Date;
390
+ duration: number;
391
+ constraint_type: ConstraintType | null;
392
+ constraint_date: Date | null;
393
+ auto_scheduling: boolean;
394
+ calendar_id: CalendarId$1 | null;
395
+ /**
396
+ * True when the activity had no valid calendar of its own and inherited the
397
+ * project default at load (its backend `calendarId` was null/undefined or a
398
+ * dangling reference). Set by `parseFromBackend` when `resolveCalendarId`
399
+ * falls back. The legacy DHTMLX worker computed this; the core owns calendar
400
+ * assignment now, so it owns the flag. The UI uses it to surface
401
+ * default-assigned activities. See `CALENDARS.md`.
402
+ */
403
+ assignedDefaultCalendar?: boolean;
404
+ progress: number;
405
+ cost: number;
406
+ used_cost: number;
407
+ real_cost: number;
408
+ hhWorkTime: number;
409
+ real_work: number;
410
+ ponderator: number;
411
+ correlative_id: number;
412
+ is_lookahead: boolean;
413
+ /**
414
+ * Bit de visibilidad del view-model (filtros de UI); ex `should_be_showed`
415
+ * del task DHTMLX. `visible !== false` = visible; default true. Escrito por
416
+ * el intent `visibility-set` (prop-only) y por `parseFromBackend` al load.
417
+ */
418
+ visible?: boolean;
419
+ newActivitiesArray: ActivityId$1[];
420
+ activityModifications: unknown[];
421
+ /**
422
+ * Backend DB row id (distinct from the core `id`, which is `unique_id`).
423
+ * Captured from `a.id` in `parseActivity`; `undefined` for activities
424
+ * created in-session (no backend id yet). Used by save dirty-detection to
425
+ * tell persisted activities from new ones. Mirrors `Link.proplannerId`.
426
+ */
427
+ proplannerId?: number;
428
+ unique_correlative_id: string;
429
+ ganttId: number;
430
+ sectorId: number;
431
+ companyId: number;
432
+ subcontractId: number | null;
433
+ subcontract: unknown;
434
+ custom_id: string | null;
435
+ hasCustomPonderator: boolean;
436
+ has_childs: boolean;
437
+ sumOfDurationRecursively: number;
438
+ /**
439
+ * Critical-path outputs (poblados cuando corre el CP: en background tras la
440
+ * carga vía `criticalPathReady`, o tras cada dispatch). `undefined` si el CP
441
+ * aún no corrió. `is_critical` es string DHTMLX (`'Si'`/`'No'`) post-CP,
442
+ * boolean recién parseado del backend — ver `CriticalFlag`.
443
+ */
444
+ earlyStart?: Date | null;
445
+ earlyFinish?: Date | null;
446
+ lateStart?: Date | null;
447
+ lateFinish?: Date | null;
448
+ totalSlack?: number | null;
449
+ freeSlack: number | null;
450
+ is_critical: CriticalFlag;
451
+ hasNewActivities: boolean;
452
+ isNewActivity: true | null;
453
+ /**
454
+ * Active-baseline snapshots for this activity (backend
455
+ * `sectorbaselinepoint[]`). Passthrough — carried verbatim so the
456
+ * frontend baseline columns and the ponderator computation can read the
457
+ * ACTIVE point via `getActiveBaseline`. `undefined` when the activity has
458
+ * no baseline (e.g. created in-session after the snapshot).
459
+ */
460
+ baseline_points?: BaselinePoint[];
461
+ tasks: unknown[];
462
+ responsables: unknown[];
463
+ tags: unknown[];
464
+ /**
465
+ * Pending Schedule Impact Requests (SIRs) freezing this activity. The
466
+ * schedule load path (`sector.controller.js`) already filters to
467
+ * `state==='pending'` and sends `{ id }` only, so the core stores just the
468
+ * id — enough to name the SIR when emitting a `sir-auto-reject` effect.
469
+ * Non-empty ⇒ the activity is frozen (`isFrozen`). The full SIR record lives
470
+ * in the front's Redux, not here. See `SIR_MAP.md`.
471
+ */
472
+ pendingRequests: PendingRequest[];
473
+ createdAt: string;
474
+ updatedAt: string;
475
+ }
476
+ interface Link {
477
+ id: LinkId;
478
+ source: ActivityId$1;
479
+ target: ActivityId$1;
480
+ type: LinkType;
481
+ lag: number;
482
+ ganttId?: number;
483
+ sectorId?: number;
484
+ /** BE row id (distinct from the schedule-level `id`/`unique_id`). */
485
+ proplannerId?: number;
486
+ }
487
+ interface CalendarWorktime {
488
+ /**
489
+ * Per-weekday working hours, length 7 — index 0 = Sunday.
490
+ *
491
+ * - `false` → non-working day
492
+ * - `string[]` → working day with these `"HH:MM-HH:MM"` windows
493
+ * (supports per-day variation, e.g. a short Friday,
494
+ * and multi-shift split days like `["8-12","13-17"]`)
495
+ *
496
+ * Single source of truth: this is exactly what `parseFromBackend` emits —
497
+ * a working day ALWAYS carries its resolved windows (the parser resolves
498
+ * the default-hours fallback at parse time). The vendored work-calendar
499
+ * engine natively also accepts a boolean `true` per day (DHTMLX
500
+ * contract), but that form never appears in the domain `Calendar`; it is
501
+ * reserved for the engine-facing internals (`SnapshotWorktime`,
502
+ * synthetic replay fixtures).
503
+ *
504
+ * The work-calendar engine consumes each weekday independently, so
505
+ * different days can carry different windows.
506
+ */
507
+ days: Array<false | string[]>;
508
+ /**
509
+ * Default working-hour windows (`"HH:MM-HH:MM"`). Used as the engine's
510
+ * global fallback. `parseFromBackend` sets it to the first working day's
511
+ * windows.
512
+ */
513
+ hours: string[];
514
+ /**
515
+ * Per-date exceptions: holidays (non-working) and date-level shift
516
+ * overrides. Keys are `Date.UTC(year, month, day)` (UTC midnight epoch
517
+ * ms) stringified — this matches the lookup the work-calendar engine
518
+ * performs internally at `calendar.dates[dateValue]`.
519
+ *
520
+ * Value semantics:
521
+ * - `false` → the day is non-working
522
+ * - `string[]` → use these `"HH:MM-HH:MM"` windows instead
523
+ * of the default `hours`
524
+ *
525
+ * Source: `BackendCalendar.exceptiondays[]` after the adapter expands
526
+ * `from_date`/`to_date` ranges into individual UTC days.
527
+ */
528
+ dates?: Record<string, false | string[]>;
529
+ /** Optional per-week overrides for advanced calendars. */
530
+ customWeeks?: Record<string, unknown>;
531
+ }
532
+ interface Calendar {
533
+ id: CalendarId$1;
534
+ /** Display name. Used by the DHTMLX calendar-selector column. */
535
+ name: string;
536
+ worktime: CalendarWorktime;
537
+ is_default: boolean;
538
+ /**
539
+ * True for "base" calendars (baseline comparison). The backend ships
540
+ * these with an `id` suffixed `-base` and `baseDefault` instead of
541
+ * `is_default`. The bridge uses this to set `gantt.defaultBaseCalendar`
542
+ * and to keep base calendars out of the selector dropdown.
543
+ */
544
+ baseDefault?: boolean;
545
+ /**
546
+ * Date-keyed map of exceptions (holidays, working overrides).
547
+ * Keys are `YYYY-MM-DD` UTC.
548
+ */
549
+ exceptions?: Record<string, {
550
+ working: boolean;
551
+ hours?: string[];
552
+ }>;
553
+ }
554
+ /** Sector metadata the core respects from the backend. */
555
+ interface SectorMetadata {
556
+ id: string;
557
+ name: string;
558
+ hoursPerDay: number;
559
+ hoursPerWeek: number;
560
+ dateFormat: string;
561
+ projectId: number;
562
+ companyId: number;
563
+ /**
564
+ * Project's `custom_id_prefix` (Outbuild project setting). Used by the
565
+ * CustomIdTracker to generate ids for new activities. Falls back to
566
+ * `'A'` if not provided.
567
+ */
568
+ customIdPrefix?: string;
569
+ /**
570
+ * Project's `custom_id_increment` (Outbuild project setting), as a
571
+ * string so leading zeros are preserved. Used by the CustomIdTracker
572
+ * to choose the next suffix step. Falls back to `'10'` if not provided.
573
+ */
574
+ customIdIncrement?: string;
575
+ /**
576
+ * Legacy Primavera-import flag. When true, the core recomputes each
577
+ * activity's `duration` from its start/end against its calendar at load
578
+ * (Primavera stores end-driven durations that may not match the
579
+ * calendar). Mirrors the legacy `fixPrimaveraDurations`. See CALENDARS.md.
580
+ */
581
+ updateDurationForPrimaveraEndDate?: boolean;
582
+ /**
583
+ * Project's `activity_creter` — criterion for ponderator redistribution
584
+ * (`'DURATION' | 'COST' | 'HH'`). Lives at the project level; arrives via
585
+ * the sector payload's pass-through bucket. `parseSector` always sets it,
586
+ * defaulting to `'DURATION'` (the backend default) when absent/unknown.
587
+ */
588
+ activityCreter?: ActivityCreter;
589
+ /**
590
+ * Project's status criterion for the `status` column — `'Baseline'` compares
591
+ * `progress` against `expected_progress_base`, `'Actual'` against
592
+ * `expected_progress`. Mutable at runtime via `ScheduleCore.setStatusCriteria`
593
+ * (the Baseline↔Actual toggle). Not a backend field (client-only session
594
+ * setting); a complete sector always carries it — `parseSector` sets the
595
+ * default and `resolveStatusCriteria` normalizes the client override at init.
596
+ * See `docs/domain/columns/COLUMN_status.md`.
597
+ */
598
+ statusCriteria: StatusCriteria;
599
+ }
600
+ /**
601
+ * A single entity mutation record emitted by a dispatch ChangeSet.
602
+ * Layer-neutral: contains no dispatch-specific dependencies and is used
603
+ * both by `dispatch/` and by `internal/tracking/`.
604
+ */
605
+ interface EntityChange<T> {
606
+ id: string;
607
+ kind: 'created' | 'updated' | 'deleted';
608
+ fields?: Record<string, {
609
+ before: unknown;
610
+ after: unknown;
611
+ }> | undefined;
612
+ after: Readonly<T> | null;
613
+ }
614
+
615
+ /**
616
+ * Public dispatch types — see Q8/Q9/Q10 in SCHEDULE_CORE_API.md.
617
+ *
618
+ * Supported kinds:
619
+ * - `inline-edit` — edit a single column on an activity.
620
+ * - `link-create` — add a link between two activities.
621
+ * - `link-update` — change the lag and/or type of an existing link.
622
+ * - `link-delete` — remove a link by id.
623
+ * - `activity-create` — create a new activity (root, child, or sibling).
624
+ * - `activity-delete` — cascade-delete activities + incident links.
625
+ * - `activity-move` — reorder among siblings or move to a different parent.
626
+ * - `activity-indent` — multi-select indent (children of previous non-selected sibling).
627
+ * - `activity-outdent` — multi-select outdent (move to grandparent level).
628
+ * - `activity-set-progress` — complete/uncomplete (0/100) with cascade.
629
+ * - `selection-toggle` / `selection-replace` — checkbox selection (prop-only).
630
+ * - `activity-paste` — bulk paste (N activities + M links, one atomic ChangeSet).
631
+ * - `dates-batch` — K date edits as one unit of work (multi-drag).
632
+ * - `bulk-edit` — K edits on any pipeline column as one unit of work (bulk editors).
633
+ * - `links-batch` — K link create/update/delete as one atomic unit (mass link/unlink buttons).
634
+ */
635
+ type LinksBatchOperation = {
636
+ kind: 'create';
637
+ source: ActivityId$1;
638
+ target: ActivityId$1;
639
+ type: LinkType;
640
+ lag?: number;
641
+ linkId?: LinkId;
642
+ } | {
643
+ kind: 'update';
644
+ linkId: LinkId;
645
+ type?: LinkType;
646
+ lag?: number;
647
+ } | {
648
+ kind: 'delete';
649
+ linkId: LinkId;
650
+ };
651
+
652
+ type DispatchAction = {
653
+ kind: 'inline-edit';
654
+ activityId: ActivityId$1;
655
+ column: string;
656
+ newValue: unknown;
657
+ } | {
658
+ /**
659
+ * K ediciones de fecha como UNA unidad de trabajo (multi-drag): cada
660
+ * edit corre su pipeline individual (verdict por barra), pero el
661
+ * post-mutation (autoscheduler → bounds → CP) corre UNA sola vez y
662
+ * sale UN ChangeSet. Patrón hermano de `activity-paste` /
663
+ * `dispatchInlineEditLinks`. Track onAfterTaskDrag F2.2.
664
+ */
665
+ kind: 'dates-batch';
666
+ edits: ReadonlyArray<{
667
+ activityId: ActivityId$1;
668
+ column: 'start_date' | 'end_date';
669
+ newValue: unknown;
670
+ }>;
671
+ } | {
672
+ /**
673
+ * K edits on ANY pipeline column as ONE unit of work — the
674
+ * generalization of `dates-batch` to the 16 registry columns
675
+ * (schedule bulk editors / modals; PLAN_convergencia_wiring §4.1).
676
+ * Each edit resolves its pipeline via `findPipelineForColumn` and
677
+ * runs phases 1-4 with an individual verdict (a rejected edit does
678
+ * NOT abort the others — paste semantics). A column without a
679
+ * pipeline (e.g. the link columns `custom_predecessors` /
680
+ * `custom_sucessors`, which mutate the link graph and dispatch as
681
+ * link intents) rejects ONLY that edit. The post-mutation pass
682
+ * (autoscheduler → parent bounds → CP) runs ONCE at the end and a
683
+ * single ChangeSet comes out = ONE undo step (never coalesces).
684
+ * Supports both shapes: K activities × same column, and 1 activity
685
+ * × several columns (atomic multi-field edit).
686
+ */
687
+ kind: 'bulk-edit';
688
+ edits: ReadonlyArray<{
689
+ activityId: ActivityId$1;
690
+ column: string;
691
+ newValue: unknown;
692
+ }>;
693
+ eventSource?: string;
694
+ } | {
695
+ kind: 'link-create';
696
+ source: ActivityId$1;
697
+ target: ActivityId$1;
698
+ type: LinkType;
699
+ /** Lag in DAYS (public boundary unit; converted to working hours inside). */
700
+ lag: number;
701
+ /** Optional explicit id (tests pass deterministic ids). */
702
+ linkId?: LinkId;
703
+ } | {
704
+ kind: 'link-update';
705
+ linkId: LinkId;
706
+ /** New type. Omit to preserve. */
707
+ type?: LinkType;
708
+ /** New lag in DAYS (public boundary unit). Omit to preserve. */
709
+ lag?: number;
710
+ } | {
711
+ kind: 'link-delete';
712
+ linkId: LinkId;
713
+ } | {
714
+ kind: 'links-batch';
715
+ operations: ReadonlyArray<LinksBatchOperation>;
716
+ } | {
717
+ kind: 'activity-create';
718
+ /** Where to put the new activity. Use `'0'` for root. */
719
+ parentId: ActivityId$1 | '0';
720
+ /**
721
+ * Optional — insert the new activity immediately AFTER this sibling.
722
+ * If omitted (and `beforeSiblingId` also omitted), the new activity
723
+ * is appended as the last child of `parentId`. The sibling must
724
+ * currently be a child of `parentId`, otherwise the dispatch is
725
+ * rejected. Mutually exclusive with `beforeSiblingId`.
726
+ */
727
+ afterSiblingId?: ActivityId$1 | undefined;
728
+ /**
729
+ * Optional — insert the new activity immediately BEFORE this sibling.
730
+ * Use case: "insert as first child" (`beforeSiblingId = currentFirstChild`)
731
+ * or "insert at position N" (`beforeSiblingId = children[N]`).
732
+ * The sibling must currently be a child of `parentId`, otherwise the
733
+ * dispatch is rejected. Mutually exclusive with `afterSiblingId`.
734
+ */
735
+ beforeSiblingId?: ActivityId$1 | undefined;
736
+ /**
737
+ * Optional field overrides applied on top of the defaults built by
738
+ * the creation pipeline. Common cases: `text`, `duration`,
739
+ * `start_date`, `constraint_type`, `constraint_date`, `calendar_id`.
740
+ *
741
+ * Only keys that name a real model field are honored — the pipeline
742
+ * drops any incidental non-model key (dhtmlx runtime junk, lightbox UI
743
+ * state) so it can never phantom onto the created activity. `name` is
744
+ * accepted as an alias for `text` (mapped, then dropped as a raw key).
745
+ * See `isDroppableOverrideKey` in `creation/defaults.ts`.
746
+ */
747
+ overrides?: Partial<Activity>;
748
+ /**
749
+ * Deterministic activity id — used by tests and by replay flows
750
+ * (paste, undo) that need to assign a specific id. If omitted, the
751
+ * core's internal id generator allocates one.
752
+ */
753
+ activityId?: ActivityId$1;
754
+ /**
755
+ * Where this create originated from — passed through to the
756
+ * `schedule_activity_creation` Amplitude event. Free-form string
757
+ * (matches legacy `INSERT_EVENT_SOURCES`).
758
+ */
759
+ eventSource?: string | undefined;
760
+ } | {
761
+ kind: 'activity-delete';
762
+ /**
763
+ * IDs to delete. Each id triggers a cascade delete of its entire
764
+ * subtree. Links incident to any deleted activity are removed too.
765
+ * Operation is atomic — if any id is missing, the whole dispatch is
766
+ * rejected before mutating anything.
767
+ */
768
+ activityIds: ReadonlyArray<ActivityId$1>;
769
+ eventSource?: string;
770
+ } | {
771
+ kind: 'activity-move';
772
+ /**
773
+ * The activity to move. Single-id only — for batch reorder use
774
+ * multiple dispatches (each one observes the post-previous state).
775
+ */
776
+ activityId: ActivityId$1;
777
+ /** Destination parent. Same as current parent = reorder within. */
778
+ parentId: ActivityId$1 | '0';
779
+ /**
780
+ * Optional — position the moved activity immediately below this
781
+ * sibling. If omitted, appended as the last child of `parentId`.
782
+ * The sibling must currently be a child of `parentId`; otherwise
783
+ * the dispatch is rejected.
784
+ */
785
+ afterSiblingId?: ActivityId$1;
786
+ eventSource?: string;
787
+ } | {
788
+ kind: 'activity-indent';
789
+ /**
790
+ * IDs to indent. Multi-select honors chain semantics: each id is
791
+ * moved under its closest previous sibling that is NOT in the
792
+ * selected set, so consecutive selected siblings all land under the
793
+ * same anchor (matches legacy `findPreviousNonSelectedSibling`).
794
+ */
795
+ activityIds: ReadonlyArray<ActivityId$1>;
796
+ eventSource?: string;
797
+ } | {
798
+ kind: 'activity-outdent';
799
+ /**
800
+ * IDs to outdent. Multi-select preserves relative order among the
801
+ * selected siblings via initial-index capture (matches legacy
802
+ * `initialFirstSiblings` + `initialTaskIndexes` pre-loop setup).
803
+ */
804
+ activityIds: ReadonlyArray<ActivityId$1>;
805
+ eventSource?: string;
806
+ } | {
807
+ /**
808
+ * Set progress to 0 or 100 with recursive cascade semantics.
809
+ *
810
+ * Triggered by the schedule's "complete" / "uncomplete" buttons.
811
+ * **Distinct intent from `inline-edit`** — bypasses the
812
+ * `isSummaryActivity` gate of `progressPipeline.canEdit` because
813
+ * the user is explicitly invoking the recursive operation
814
+ * ("complete this parent and all its descendants"), not editing
815
+ * a single cell.
816
+ *
817
+ * Inline edits on the progress column still use `inline-edit` with
818
+ * full `canEdit`/`validate` gating (which blocks summary activities
819
+ * by design). See [[complete activity business rules]] R8 and the
820
+ * bifurcation rationale.
821
+ *
822
+ * Internally reuses the `progressPipeline.transform` so cascade,
823
+ * rollup, tracking and visualization are identical to inline-edit
824
+ * with newValue=0 or newValue=100.
825
+ */
826
+ kind: 'activity-set-progress';
827
+ activityId: ActivityId$1;
828
+ /** Strictly 0 (uncomplete) or 100 (complete). */
829
+ newValue: 0 | 100;
830
+ eventSource?: string;
831
+ } | {
832
+ /**
833
+ * Checkbox selection toggle (2026-06-06). The core is the
834
+ * authority of the check state (`checked` / `visibleChecked` /
835
+ * `mustApplyVisibleChecked` written as a single bit): it runs the
836
+ * full propagation (descendants cascade, frozen partition with
837
+ * parent blocking, unified upward pass — see
838
+ * `internal/selection/compute-selection-update.ts`) and emits a
839
+ * prop-only ChangeSet the bridge applies in bulk to DHTMLX
840
+ * (silent store write + viewport repaint). No autoscheduler, no
841
+ * critical path.
842
+ */
843
+ kind: 'selection-toggle';
844
+ activityId: ActivityId$1;
845
+ /** Checkbox state AFTER the user click. */
846
+ isChecked: boolean;
847
+ /**
848
+ * Ids frozen by pending SIRs, computed by the caller from the
849
+ * runtime (DHTMLX task props — the core's own `pendingRequests`
850
+ * is load-time-stale until TODO #10). When omitted, the core
851
+ * falls back to its own snapshots.
852
+ */
853
+ frozenIds?: ReadonlyArray<ActivityId$1>;
854
+ /**
855
+ * Enable the upward auto-check of ancestors when all their
856
+ * checkable children become selected. Mirrors the (inverted)
857
+ * CHECKBOX_NOT_AUTOMATIC_PARENT_SELECTION flag. Default: false.
858
+ */
859
+ autoCheckParents?: boolean;
860
+ eventSource?: string;
861
+ } | {
862
+ /**
863
+ * Replace the whole selection with an exact id set — no
864
+ * propagation. Used by clear / block-selection / external sync.
865
+ * Unknown ids are ignored. Same prop-only ChangeSet contract as
866
+ * `selection-toggle`.
867
+ */
868
+ kind: 'selection-replace';
869
+ activityIds: ReadonlyArray<ActivityId$1>;
870
+ eventSource?: string;
871
+ } | {
872
+ /**
873
+ * View-model visibility set (Fase 1, 2026-07-04). ABSOLUTE set: the
874
+ * UI sends the COMPLETE list of ids visible after its filters run;
875
+ * the core diffs against the current `visible` bits (an id changes
876
+ * when `(visible !== false) !== willBeVisible`) and emits a
877
+ * prop-only ChangeSet carrying ONLY the `visible` field for the
878
+ * activities that changed. Unknown ids are ignored. Same prop-only
879
+ * contract as `selection-replace`: no autoscheduler, no critical
880
+ * path, no undo step. Idempotent.
881
+ */
882
+ kind: 'visibility-set';
883
+ visibleIds: ReadonlyArray<ActivityId$1>;
884
+ eventSource?: string;
885
+ } | {
886
+ /**
887
+ * Sync the pending-SIR set for one activity into the core
888
+ * (2026-06-08). ABSOLUTE set: the bridge sends the activity's current
889
+ * pending requests whenever a SIR is created/resolved in the front,
890
+ * so the core's freeze bit stays fresh (closes TODO #10) and is the
891
+ * standalone authority for selection gating + auto-reject. Prop-only
892
+ * ChangeSet; no autoscheduler, no CP. Idempotent.
893
+ */
894
+ kind: 'sir-sync';
895
+ activityId: ActivityId$1;
896
+ pendingRequests: ReadonlyArray<PendingRequest>;
897
+ eventSource?: string;
898
+ } | {
899
+ /**
900
+ * Bulk paste (2026-06-08). Creates N activities (preserving the
901
+ * copied subtree hierarchy) plus M internal links in a SINGLE
902
+ * atomic ChangeSet — the future unit of undo. Replaces the
903
+ * DHTMLX-side paste that bypassed the core via `gantt.isPasting`.
904
+ *
905
+ * The caller (bridge) supplies, per activity, the field
906
+ * `overrides` already shaped by the paste prepare step (resets +
907
+ * validated catalogs). The core allocates ids/uids, re-hangs the
908
+ * hierarchy via `originalId → newId`, promotes parents, runs the
909
+ * hh/cost cascade + correlative sweep + autoscheduler ONCE at the
910
+ * end of the batch (not per activity).
911
+ *
912
+ * See [[COPY_PASTE_CORE_MIGRATION_DESIGN]].
913
+ */
914
+ kind: 'activity-paste';
915
+ /**
916
+ * Where the pasted roots land. `{ parentId, index }` pastes INSIDE
917
+ * the reference (index 0 = first child). `{ afterSiblingId }`
918
+ * pastes as a sibling immediately below the reference; the parent
919
+ * is derived from the sibling.
920
+ */
921
+ destination: {
922
+ parentId: ActivityId$1 | '0';
923
+ index: number;
924
+ } | {
925
+ afterSiblingId: ActivityId$1;
926
+ };
927
+ /**
928
+ * The activity the paste is anchored to (the single selected row).
929
+ * Used as the custom_id reference for pasted children. Replaces
930
+ * the legacy `gantt.pasteReferenceActivity` instance flag.
931
+ */
932
+ referenceActivityId: ActivityId$1;
933
+ /**
934
+ * Activities to create, in tree order (parents before children).
935
+ * `originalId` / `originalParentId` are the ids from the copied
936
+ * payload; the core remaps them to freshly allocated ids and uses
937
+ * the remap to re-hang the hierarchy and the links.
938
+ */
939
+ activities: ReadonlyArray<PastedActivityInput>;
940
+ /**
941
+ * Internal links to recreate. `source`/`target` reference the
942
+ * ORIGINAL (copied) ids; links with an endpoint outside the
943
+ * pasted set are dropped (dangling).
944
+ */
945
+ links: ReadonlyArray<PastedLinkInput>;
946
+ eventSource?: string;
947
+ };
948
+ interface PastedActivityInput {
949
+ readonly originalId: ActivityId$1;
950
+ readonly originalParentId: ActivityId$1 | '0';
951
+ readonly overrides: Partial<Activity>;
952
+ }
953
+ interface PastedLinkInput {
954
+ readonly source: ActivityId$1;
955
+ readonly target: ActivityId$1;
956
+ readonly type: LinkType;
957
+ /** Lag in DAYS (public boundary unit; converted to working hours inside). */
958
+ readonly lag: number;
959
+ }
960
+ /** Unit of duration/lag at the public boundary. Default 'days'. */
961
+ type OutputUnit = 'days' | 'hours';
962
+ interface DispatchOptions {
963
+ /** Skip the autoscheduler pass after the mutation. Default: false. */
964
+ skipAutoSchedule?: boolean;
965
+ /** Skip the CP recompute after the mutation. Default: false. */
966
+ skipCriticalPath?: boolean;
967
+ /** 'days' default; the DHTMLX bridge passes 'hours' to skip conversion. */
968
+ outputUnit?: OutputUnit;
969
+ /**
970
+ * Unit of the INPUT `newValue` for unit-bearing inline-edit columns (today:
971
+ * duration). Default 'days' (public contract; card + tests send days). The
972
+ * DHTMLX grid editor produces the store unit (HOURS via formatter.parse), so
973
+ * the bridge passes 'hours' for those edits → the pipeline skips the days→hours
974
+ * conversion instead of double-converting.
975
+ */
976
+ inputUnit?: OutputUnit;
977
+ }
978
+ type DispatchResult = {
979
+ ok: true;
980
+ changes: ChangeSet;
981
+ /**
982
+ * Solo los intents batch (`dates-batch` / `bulk-edit`): resultado
983
+ * POR EDIT, en el orden de `edits`. Un edit rechazado no aborta a
984
+ * los demás (semántica paste); el bridge re-proyecta solo los
985
+ * rechazados.
986
+ */
987
+ verdicts?: ReadonlyArray<DatesBatchVerdict>;
988
+ /**
989
+ * INTERNAL (undo / B4). Full pre-mutation snapshot of every
990
+ * deleted/structurally-removed activity row, keyed by STRING id. The
991
+ * ChangeSet emits deleted rows as `{ kind:'deleted', after:null }` with no
992
+ * `before`, so resurrection on undo needs this. Populated by structural
993
+ * handlers (delete/move/paste); stripped before the result reaches the
994
+ * bridge in the Wave-3 integration.
995
+ */
996
+ __beforeSnap?: ReadonlyMap<string, EngineActivitySnapshot>;
997
+ /**
998
+ * INTERNAL (undo / B4). Incident links of deleted activities, keyed by
999
+ * STRING id (EntityChange link ids are String()-normalized). Carries the
1000
+ * engine `LinkSnapshot` (lag in working hours) for unit-correct re-add.
1001
+ */
1002
+ __beforeLinks?: ReadonlyMap<string, LinkSnapshot>;
1003
+ } | {
1004
+ ok: false;
1005
+ reason: string;
1006
+ alertKey?: string | undefined;
1007
+ };
1008
+ type DatesBatchVerdict = {
1009
+ activityId: string;
1010
+ ok: true;
1011
+ } | {
1012
+ activityId: string;
1013
+ ok: false;
1014
+ reason: string;
1015
+ alertKey?: string | undefined;
1016
+ };
1017
+
1018
+ interface TrackingEvent {
1019
+ name: string;
1020
+ properties: Record<string, unknown>;
1021
+ }
1022
+ /**
1023
+ * Side effects a dispatch emits that are NOT entity-field changes and NOT
1024
+ * Amplitude tracking — commands the bridge must act on. Today: SIR
1025
+ * auto-reject (a frozen activity's dates changed or it was deleted, so its
1026
+ * pending SIR must be rejected). The core owns the rule; the bridge consumes
1027
+ * the effect (Redux REJECTED; durable persistence rides the activity save).
1028
+ * See `SIR_MAP.md` §10.
1029
+ */
1030
+ type ScheduleEffect = {
1031
+ kind: 'sir-auto-reject';
1032
+ activityId: ActivityId$1;
1033
+ /** Backend SIR id == the front Redux item id (see parseFromBackend A1). */
1034
+ sirId: string;
1035
+ reason: 'date_changed' | 'activity_deleted';
1036
+ };
1037
+ /**
1038
+ * A direct user edit violated the activity's own (pre-edit) constraint. Per
1039
+ * the canonical model (PRD constraint violations, R2/R3): the edit APPLIES
1040
+ * anyway — the warning is data for the UI (constraint validation modal),
1041
+ * never a rejection, and it is only emitted from direct edits (the
1042
+ * autoscheduler never violates, R1). Decision D-A 2026-07-04.
1043
+ */
1044
+ interface ConstraintWarning {
1045
+ kind: 'constraint_violation';
1046
+ activityId: ActivityId$1;
1047
+ constraintType: ConstraintType;
1048
+ constraintDate: Date;
1049
+ /** The projected date that violated (start or end, per constraint type). */
1050
+ projectedDate: Date;
1051
+ messageKey: string;
1052
+ }
1053
+ interface ChangeSet {
1054
+ source: DispatchAction | {
1055
+ kind: 'init';
1056
+ } | {
1057
+ kind: 'undo';
1058
+ } | {
1059
+ kind: 'redo';
1060
+ };
1061
+ activities: ReadonlyArray<EntityChange<Activity>>;
1062
+ links: ReadonlyArray<EntityChange<Link>>;
1063
+ calendars: ReadonlyArray<EntityChange<Calendar>>;
1064
+ trackingEvents: ReadonlyArray<TrackingEvent>;
1065
+ /**
1066
+ * Optional — present only when the dispatch produced side effects. Builders
1067
+ * that emit none omit it; consumers read `changeSet.effects ?? []`. Kept
1068
+ * optional to avoid churning every ChangeSet construction site. SIR (A4).
1069
+ */
1070
+ effects?: ReadonlyArray<ScheduleEffect>;
1071
+ /**
1072
+ * Optional — present only when a direct edit violated a constraint (see
1073
+ * `ConstraintWarning`). Consumers read `changeSet.warnings ?? []`.
1074
+ */
1075
+ warnings?: ReadonlyArray<ConstraintWarning>;
1076
+ }
1077
+
1078
+ interface ProjectWorkHours {
1079
+ /** Earliest start of the working day across the week, `"HH:MM"`. */
1080
+ startHour: string;
1081
+ /** Latest end of the working day across the week, `"HH:MM"`. */
1082
+ endHour: string;
1083
+ }
1084
+
1085
+ /**
1086
+ * Backend payload shapes — exactly as the Outbuild server returns.
1087
+ *
1088
+ * The core accepts these directly in the constructor and parses them into
1089
+ * normalized internal types. Keeping these as the public input type means
1090
+ * any consumer of the schedule API (frontend, node_server, future) speaks
1091
+ * the same contract.
1092
+ *
1093
+ * Validated against the real mocks in
1094
+ * `packages/schedule-core/src/testing/projects/startDate/`.
1095
+ */
1096
+ /** Calendar shift definition — one shift per day, CSV-encoded strings. */
1097
+ interface BackendShift {
1098
+ id: number;
1099
+ /**
1100
+ * Encoded shift string for the week:
1101
+ * `false,8:00,8:00,8:00,8:00,8:00,false-false,16:00,...,false`
1102
+ * left side = starts, right side = ends, each CSV is Sun..Sat.
1103
+ * `false` means non-working day.
1104
+ */
1105
+ shift_string: string;
1106
+ correlative_id: number;
1107
+ createdAt: string;
1108
+ updatedAt: string;
1109
+ calendarId: number;
1110
+ calendarexceptiondayId: number | null;
1111
+ }
1112
+ /** Backend calendar exception (per-date override). */
1113
+ interface BackendCalendarException {
1114
+ id: number;
1115
+ date: string;
1116
+ /** Optional shifts for this specific exception. */
1117
+ shifts?: BackendShift[];
1118
+ [key: string]: unknown;
1119
+ }
1120
+ /** Calendar as returned by the server. */
1121
+ interface BackendCalendar {
1122
+ id: number;
1123
+ name: string;
1124
+ unique_id: number;
1125
+ is_default: boolean;
1126
+ /** CSV of Sun..Sat: `"0,1,1,1,1,1,0"` (0 = non-working, 1 = working). */
1127
+ working_days: string;
1128
+ /** CSV of shift starts per day: `"false,8:00,8:00,8:00,8:00,8:00,false"`. */
1129
+ shift_start: string;
1130
+ /** CSV of shift ends per day: `"false,16:00,...,false"`. */
1131
+ shift_end: string;
1132
+ exceptiondays: BackendCalendarException[];
1133
+ shifts: BackendShift[];
1134
+ status: boolean;
1135
+ companyId: number;
1136
+ ganttId: number | null;
1137
+ userId: number;
1138
+ sectorId: number;
1139
+ createdAt: string;
1140
+ updatedAt: string;
1141
+ [key: string]: unknown;
1142
+ }
1143
+ /**
1144
+ * Activity exactly as the Outbuild server returns it — the raw, untrusted
1145
+ * input boundary. Dates are strings, `duration` is in backend DAYS, and field
1146
+ * names are the backend's (snake_case, `unique_id`, `parent_id`, `name`).
1147
+ *
1148
+ * `parseFromBackend` is the only place that consumes it: it validates and
1149
+ * normalizes each row into the internal model. Nothing else in the core should
1150
+ * read a `BackendActivityInput` after parse.
1151
+ */
1152
+ interface BackendActivityInput {
1153
+ /** Database row id. Distinct from `unique_id`. */
1154
+ id: number;
1155
+ /** Canonical schedule id (used by links and DHTMLX). String-encoded number. */
1156
+ unique_id: string;
1157
+ /** Parent's `unique_id`, or `"0"` for root. */
1158
+ parent_id: string;
1159
+ unique_correlative_id: string;
1160
+ correlative_id: number;
1161
+ name: string;
1162
+ description: string;
1163
+ type: 'project' | 'task' | 'milestone';
1164
+ /** Format: `"YYYY/MM/DD H:MM"` (server local, see dateFormat in sector). */
1165
+ start_date: string;
1166
+ end_date: string;
1167
+ duration: number;
1168
+ /**
1169
+ * Constraint label in plain English: `"As soon As Possible"`,
1170
+ * `"Start no earlier than"`, etc. Mapped to internal `ConstraintType`.
1171
+ */
1172
+ constraint: string;
1173
+ /** Optional constraint date — server uses ISO 8601 or null. */
1174
+ constraint_date?: string | null;
1175
+ progress: number;
1176
+ cost: number;
1177
+ used_cost: number;
1178
+ real_cost: number;
1179
+ hhWorkTime: number;
1180
+ real_work: number;
1181
+ ponderator: number;
1182
+ hasCustomPonderator: boolean;
1183
+ has_childs: boolean;
1184
+ isOnLookahead: boolean;
1185
+ sumOfDurationRecursively: number;
1186
+ freeSlack: number | null;
1187
+ is_critical: boolean;
1188
+ newActivitiesArray: string;
1189
+ hasNewActivities: boolean;
1190
+ isNewActivity: true | null;
1191
+ custom_id: string | null;
1192
+ ganttId: number;
1193
+ sectorId: number;
1194
+ companyId: number;
1195
+ calendarId: number;
1196
+ subcontractId: number | null;
1197
+ /**
1198
+ * Visibility bit of the legacy view-model. Historically NEVER sent by the
1199
+ * backend (0 of ~105k activities across the captured seeds carry it) — the
1200
+ * legacy client hardcoded `should_be_showed: true` at parse. Declared so
1201
+ * `parseActivity` can map it to `visible` if it ever arrives; absent/null
1202
+ * defaults to visible.
1203
+ */
1204
+ should_be_showed?: boolean;
1205
+ tasks: unknown[];
1206
+ responsables: unknown[];
1207
+ tags: unknown[];
1208
+ activitymodifications: unknown[];
1209
+ /** Pending SIRs for this activity — schedule load sends `{ id }` only. */
1210
+ pendingRequests: ReadonlyArray<{
1211
+ id: number | string;
1212
+ }>;
1213
+ subcontract: unknown;
1214
+ createdAt: string;
1215
+ updatedAt: string;
1216
+ /** Custom fields the backend may include — passed through opaquely. */
1217
+ [key: string]: unknown;
1218
+ }
1219
+ /** Link as returned by the server. */
1220
+ interface BackendLink {
1221
+ id: number | string;
1222
+ source: number | string;
1223
+ target: number | string;
1224
+ /** DHTMLX kind: `"0"` FS, `"1"` SS, `"2"` FF, `"3"` SF. */
1225
+ type: '0' | '1' | '2' | '3';
1226
+ lag: number;
1227
+ [key: string]: unknown;
1228
+ }
1229
+ /** Sector metadata — project-level config from the server. */
1230
+ interface BackendSector {
1231
+ id: number;
1232
+ name: string;
1233
+ description: string | null;
1234
+ status: boolean;
1235
+ set_current: boolean;
1236
+ visible: boolean;
1237
+ order: number;
1238
+ productive: boolean;
1239
+ hoursPerDay: number;
1240
+ hoursPerWeek: number;
1241
+ accumulatedDuration: number;
1242
+ /** Date format string used by the backend, e.g. `"DD/MM/YY hh:mm"`. */
1243
+ dateFormat: string;
1244
+ didCloseWeek: string | null;
1245
+ currentClosedWeek: string | null;
1246
+ expectedProgress: number;
1247
+ /**
1248
+ * Legacy Primavera-import flag. When true, the stored `duration` of each
1249
+ * activity may not match the calendar (Primavera stores end-driven
1250
+ * durations); the core recomputes duration from start/end at load. ~218
1251
+ * production sectors carry it (active). See `Obsidian:newPlanSchedule/CALENDARS.md`.
1252
+ */
1253
+ update_duration_for_primavera_for_end_date?: boolean | null;
1254
+ companyId: number;
1255
+ projectId: number;
1256
+ version: string;
1257
+ createdAt: string;
1258
+ updatedAt: string;
1259
+ /** Calendars nested inside sector when the sector-fetch endpoint is used. */
1260
+ calendars?: BackendCalendar[];
1261
+ [key: string]: unknown;
1262
+ }
1263
+ /** Optional sibling payloads the API often returns alongside the sector. */
1264
+ interface BackendScheduleImpactRequests {
1265
+ pending: unknown[];
1266
+ approved: unknown[];
1267
+ rejected: unknown[];
1268
+ }
1269
+ /** Complete server payload accepted by `ScheduleCore`. */
1270
+ interface BackendInput {
1271
+ sector: BackendSector;
1272
+ activities: BackendActivityInput[];
1273
+ links: BackendLink[];
1274
+ /**
1275
+ * Calendars come either inside `sector.calendars` or as a sibling array.
1276
+ * If both are present, the sibling array wins.
1277
+ */
1278
+ calendars?: BackendCalendar[] | undefined;
1279
+ /**
1280
+ * Baseline calendars, RAW from the backend's `showBaseCalendar` endpoint
1281
+ * (exceptions under `baseexceptiondays`, `is_default`, no `-base` suffix).
1282
+ * Parsed into a separate `ParsedInput.baseCalendars` collection — never
1283
+ * mixed into the scheduling calendars. The core owns their reshape /
1284
+ * differentiation (previously done by the frontend `refreshCalendarsRewrite`).
1285
+ */
1286
+ baseCalendars?: BackendCalendar[] | undefined;
1287
+ }
1288
+
1289
+ declare const COLUMN: {
1290
+ readonly REAL_WORK: "real_work";
1291
+ readonly REAL_COST: "real_cost";
1292
+ readonly EXPECTED_PROGRESS: "expected_progress";
1293
+ readonly EXPECTED_PROGRESS_BASE: "expected_progress_base";
1294
+ readonly START_BASE: "start_base";
1295
+ readonly END_BASE: "end_base";
1296
+ readonly DURATION_BASE: "duration_base";
1297
+ readonly COST_BASE: "cost_base";
1298
+ readonly WORK_BASE: "work_base";
1299
+ readonly CALENDAR_DURATION: "calendarDuration";
1300
+ readonly IS_CRITICAL: "is_critical";
1301
+ readonly EARLY_START: "earlyStart";
1302
+ readonly EARLY_FINISH: "earlyFinish";
1303
+ readonly LATE_START: "lateStart";
1304
+ readonly LATE_FINISH: "lateFinish";
1305
+ readonly FREE_SLACK: "freeSlack";
1306
+ readonly TOTAL_SLACK: "totalSlack";
1307
+ readonly CORRELATIVE_ID: "correlative_id";
1308
+ readonly UNIQUE_CORRELATIVE_ID: "unique_correlative_id";
1309
+ readonly PONDERATOR: "ponderator";
1310
+ readonly STATUS: "status";
1311
+ readonly TEXT: "text";
1312
+ readonly DESCRIPTION: "description";
1313
+ readonly DURATION: "duration";
1314
+ readonly PROGRESS: "progress";
1315
+ readonly COST: "cost";
1316
+ readonly USED_COST: "used_cost";
1317
+ readonly HH_WORK_TIME: "hhWorkTime";
1318
+ readonly START_DATE: "start_date";
1319
+ readonly END_DATE: "end_date";
1320
+ readonly CONSTRAINT_TYPE: "constraint_type";
1321
+ readonly CONSTRAINT_DATE: "constraint_date";
1322
+ readonly CALENDAR_ID: "calendar_id";
1323
+ readonly CUSTOM_ID: "custom_id";
1324
+ readonly SUBCONTRACT_ID: "subcontractId";
1325
+ readonly RESPONSABLES: "responsables";
1326
+ readonly TAGS: "tags";
1327
+ readonly CUSTOM_PREDECESSORS: "custom_predecessors";
1328
+ readonly CUSTOM_SUCESSORS: "custom_sucessors";
1329
+ };
1330
+ type ColumnName = (typeof COLUMN)[keyof typeof COLUMN];
1331
+
1332
+ type ActivityId = string;
1333
+ type CalendarId = string;
1334
+
1335
+ /**
1336
+ * Role interfaces (Interface Segregation).
1337
+ *
1338
+ * Consumers receive the smallest role they need instead of the whole
1339
+ * `ScheduleState`. A pure-read consumer takes `ActivityReader`; a
1340
+ * read+write consumer takes `ActivityWriter`; a consumer that drives the
1341
+ * per-dispatch write journal takes `WriteJournal`. `ScheduleState`
1342
+ * implements all of them, so passing it anywhere a role is expected is
1343
+ * still valid — the role just narrows what the callee is allowed to touch.
1344
+ *
1345
+ * Signatures here are copied verbatim from `ScheduleState`; the class is
1346
+ * the source of truth. If the class changes a member, change the role to
1347
+ * match (not the other way around).
1348
+ */
1349
+
1350
+ /** READ surface over the activity/link store and the hierarchy index. */
1351
+ interface ActivityReader {
1352
+ getActivity(activityId: ActivityId$2): EngineActivitySnapshot | null;
1353
+ getAllActivities(): ReadonlyArray<EngineActivitySnapshot>;
1354
+ getAllLinks(): LinkSnapshot[];
1355
+ getLink(linkId: LinkId$1): LinkSnapshot | null;
1356
+ activityExists(activityId: ActivityId$2): boolean;
1357
+ getChildren(parentId: ActivityId$2): ActivityId$2[];
1358
+ getParent(activityId: ActivityId$2): EngineActivitySnapshot | null;
1359
+ getParentId(activityId: ActivityId$2): ActivityId$2 | 0;
1360
+ isChildOf(childId: ActivityId$2, parentId: ActivityId$2): boolean;
1361
+ getAllIds(): readonly ActivityId$2[];
1362
+ activityCount(): number;
1363
+ forEachActivity(visit: (activity: EngineActivitySnapshot, id: ActivityId$2) => void): void;
1364
+ }
1365
+ /** READ + WRITE surface over the store. */
1366
+ interface ActivityWriter extends ActivityReader {
1367
+ setActivityField<Field extends keyof EngineActivitySnapshot>(activityId: ActivityId$2, field: Field, value: EngineActivitySnapshot[Field]): void;
1368
+ /** Bulk write of several fields of one activity in a single getActivity +
1369
+ * write-capture note (vs N setActivityField calls). */
1370
+ setActivityFields(activityId: ActivityId$2, fields: Partial<EngineActivitySnapshot>): void;
1371
+ addLink(link: LinkSnapshot): void;
1372
+ removeLink(linkId: LinkId$1): void;
1373
+ setLinkField<Field extends keyof LinkSnapshot>(linkId: LinkId$1, field: Field, value: LinkSnapshot[Field]): void;
1374
+ addActivity(activity: EngineActivitySnapshot): void;
1375
+ removeActivity(activityId: ActivityId$2): void;
1376
+ }
1377
+
1378
+ /**
1379
+ * Injected reporting port. The core is UI-agnostic: it cannot `console.*`,
1380
+ * show UI, or know about Sentry. When it hits a recoverable data-quality
1381
+ * issue (`warn`) or a failure it could not complete (`error`), it calls this
1382
+ * port; the consumer (react_client) implements it and decides the outcome
1383
+ * (Sentry, toast, nothing). Same injected-port pattern as `AutoSchedulerPort`.
1384
+ */
1385
+ interface ScheduleCoreReporter {
1386
+ /** Recoverable data-quality issue; the core continues with its fallback. */
1387
+ warn(message: string, context?: unknown): void;
1388
+ /** A real failure the core could not complete. */
1389
+ error(message: string, cause?: unknown): void;
1390
+ }
1391
+
1392
+ /**
1393
+ * initialize-core — the construction + load-pass orchestration extracted from
1394
+ * the `ScheduleCore` constructor. `initializeCore(input)` performs the entire
1395
+ * load sequence (parse → snapshot → state → demote → id generators →
1396
+ * custom-id tracker → optional baseline block → initial passes) and returns
1397
+ * the assembled internals (`CoreInternals`) for the facade to assign. The
1398
+ * facade constructor is assignment-only.
1399
+ *
1400
+ * The load-pass ORDER here is a CONTRACT — see `CURSOR.md`
1401
+ * §"Flujo de core.dispatch paso a paso" and the inline comments below. Do not
1402
+ * reorder, merge, or drop any step.
1403
+ *
1404
+ * The public types (`ScheduleCoreStatus`, `ScheduleCoreInput`) live here so
1405
+ * `initialize-core.ts` never imports from `scheduleCore.ts` (which imports
1406
+ * `initializeCore`), avoiding an import cycle. `scheduleCore.ts` re-exports the
1407
+ * two public types
1408
+ * so `src/index.ts`'s existing `export type { ... } from './init/schedule-core.js'`
1409
+ * keeps resolving unchanged.
1410
+ */
1411
+
1412
+ type ScheduleCoreStatus = 'ready' | 'destroyed' | 'poisoned';
1413
+ interface ScheduleCoreInput extends BackendInput {
1414
+ /**
1415
+ * Skip the full autoscheduler pass that normally runs from the
1416
+ * constructor (`runInitialPasses`).
1417
+ *
1418
+ * Backend data already ships with `start_date` / `end_date` / `duration`
1419
+ * scheduled — running the full ASAP+ALAP graph at load is redundant and
1420
+ * extremely slow on large schedules (thousands of activities + links).
1421
+ *
1422
+ * When `true`, the constructor still runs the lightweight bottom-up
1423
+ * post-processors (`updateParentBoundsFromChildren`) so parent bounds
1424
+ * and progress rollup are consistent. The autoscheduler runs lazily on
1425
+ * the first `dispatch(...)` call.
1426
+ *
1427
+ * Default: `false` (preserve existing behaviour). The bridge in
1428
+ * `react_client/` sets this to `true` for production loads.
1429
+ */
1430
+ skipInitialAutoSchedule?: boolean;
1431
+ /**
1432
+ * Baseline points (backend `sectorbaselinepoint[]`, the `activityponts`
1433
+ * endpoint payload) to overlay at load. Each point maps to an activity by
1434
+ * `point.activityId` → `activity.proplannerId`. When present, the constructor
1435
+ * overlays them so the baseline columns + `expected_progress_base` can read the
1436
+ * ACTIVE point. NB: ponderators are NOT recomputed at load (backend ships the
1437
+ * value prod renders verbatim — recomputing diverges); `goCalculatePonderators`
1438
+ * is a save-time op. Omit (or pass `[]`) for projects with no baseline.
1439
+ */
1440
+ baselinePoints?: readonly BaselinePoint[];
1441
+ /**
1442
+ * Clock port for time-dependent columns (`expected_progress*`). A FUNCTION
1443
+ * (not a frozen instant): the core calls it FRESH each time it needs "today"
1444
+ * (load + every dispatch that recomputes expected_progress), so the value
1445
+ * tracks the real day even on a long-lived session — never congelado.
1446
+ *
1447
+ * Opt-in (no default): omit it → the core does NOT compute `expected_progress`
1448
+ * (preserves the legacy path; keeps the test suite deterministic). Production
1449
+ * injects `() => new Date()`; tests inject `() => fixedDate` for reproducibility.
1450
+ * The core never calls `new Date()` itself — this port is the only time source.
1451
+ */
1452
+ clock?: () => Date;
1453
+ /**
1454
+ * Critical-path mode at LOAD. The CP is ALWAYS computed; this flag only
1455
+ * decides WHEN `core.ready` is considered settled:
1456
+ * - `false` (default — front): `core.ready` resolves WITHOUT the CP (fast
1457
+ * load); the CP runs async and is delivered via `core.criticalPathReady`.
1458
+ * - `true` (backend, no front): `core.ready` WAITS for the CP — it is part of
1459
+ * the load (blocking). The caller does a single `await core.ready` and has
1460
+ * the dates + CP fields ready.
1461
+ */
1462
+ criticalPathOnLoad?: boolean;
1463
+ /**
1464
+ * Initial status criterion (`'Baseline' | 'Actual'`) for the `status` column.
1465
+ * Not a backend field (client-only view pref); the client passes the current
1466
+ * value, defaulting to `'Baseline'`. Overrides the sector default when present.
1467
+ * Changed at runtime via `ScheduleCore.setStatusCriteria`.
1468
+ */
1469
+ statusCriteria?: StatusCriteria;
1470
+ /**
1471
+ * Optional injected port for data-quality warnings and scheduling errors.
1472
+ * Defaults to a no-op when absent (the core never touches console/Sentry).
1473
+ */
1474
+ reporter?: ScheduleCoreReporter;
1475
+ }
1476
+
1477
+ declare class ScheduleCore {
1478
+ private _status;
1479
+ /** True while an async dispatch is suspended inside the write-capture window.
1480
+ * A sync facade mutation running in that gap would corrupt the in-flight
1481
+ * journal, so sync mutators refuse to run while it is set. */
1482
+ private _dispatchInFlight;
1483
+ private readonly coreRuntime;
1484
+ private readonly _undo;
1485
+ private _opQueue;
1486
+ private _mutationEpoch;
1487
+ private _cpComputedEpoch;
1488
+ constructor(input: ScheduleCoreInput);
1489
+ get ready(): Promise<void>;
1490
+ get criticalPathReady(): Promise<{
1491
+ changes: ChangeSet;
1492
+ }>;
1493
+ get status(): ScheduleCoreStatus;
1494
+ getSector(): Readonly<SectorMetadata>;
1495
+ getActivityView(id: ActivityId$1, unit?: OutputUnit): Readonly<Activity> | null;
1496
+ getAllActivitiesView(unit?: OutputUnit): ReadonlyArray<Readonly<Activity>>;
1497
+ getChildrenView(parentId: ActivityId$1 | '0', unit?: OutputUnit): ReadonlyArray<Readonly<Activity>>;
1498
+ getLinkView(id: LinkId, unit?: OutputUnit): Readonly<Link> | null;
1499
+ getAllLinksView(unit?: OutputUnit): ReadonlyArray<Readonly<Link>>;
1500
+ getProperty<Property extends keyof Activity>(activityId: ActivityId$1, property: Property, options?: {
1501
+ unit?: OutputUnit;
1502
+ }): Activity[Property] | null;
1503
+ getAllIds(): readonly string[];
1504
+ forEachActivityId(visit: (id: string) => void): void;
1505
+ getChildrenIds(parentId: ActivityId$1 | '0'): ReadonlyArray<ActivityId$1>;
1506
+ getSelectedActivityIds(): string[];
1507
+ /**
1508
+ * Every activity id in canonical DFS visual order (pre-order from roots,
1509
+ * siblings by `correlative_id` ASC) — ALL activities; filtering by
1510
+ * `visible` is the consumer's job. Memoized in the state and invalidated
1511
+ * on any structural mutation (add/remove/reparent/renumber), so repeated
1512
+ * reads between mutations are O(1).
1513
+ */
1514
+ getVisualOrderIds(): ReadonlyArray<ActivityId$1>;
1515
+ hasChild(parentId: ActivityId$1 | '0'): boolean;
1516
+ getCalendar(id: CalendarId$1): Readonly<Calendar> | null;
1517
+ getAllCalendars(): ReadonlyArray<Readonly<Calendar>>;
1518
+ getBaseCalendars(): ReadonlyArray<Readonly<Calendar>>;
1519
+ isCustomIdInUse(customId: string, currentCustomId?: string | null): boolean;
1520
+ getProjectWorkHours(): ProjectWorkHours | null;
1521
+ getModifiedLinks(): ReadonlyArray<Readonly<Link>>;
1522
+ markLinksPersisted(): void;
1523
+ getModifiedActivities(): ReadonlyArray<Readonly<Activity>>;
1524
+ markActivitiesPersisted(): void;
1525
+ applyBaselines(points: readonly BaselinePoint[]): ReadonlyArray<Readonly<Activity>>;
1526
+ /**
1527
+ * Whole-tree ponderator recompute (zeroes, then redistributes 100 within
1528
+ * each parent from its children's active baselines, weighted by `criterion`).
1529
+ * Returns the activities whose `ponderator` changed, for a prop-only repaint.
1530
+ * The base rollup (`applyBaselines`) depends on fresh ponderators, so callers
1531
+ * run this before it on a criterion change or baseline save.
1532
+ */
1533
+ recomputeAllPonderators(criterion: ActivityCreter): ReadonlyArray<Readonly<Activity>>;
1534
+ /**
1535
+ * Recompute the weighted-progress rollup of every parent, bottom-up.
1536
+ * Returns the activities whose `progress` changed, for a prop-only repaint.
1537
+ */
1538
+ recomputeAllProgressRollup(): ReadonlyArray<Readonly<Activity>>;
1539
+ /**
1540
+ * Set the project's status criterion (the Baseline↔Actual toggle) and
1541
+ * recompute every activity's `status` against it. Stores the criterion so
1542
+ * future dispatches keep deriving status with it. Returns the activities
1543
+ * whose `status` changed, for a prop-only repaint.
1544
+ */
1545
+ setStatusCriteria(criteria: StatusCriteria): ReadonlyArray<Readonly<Activity>>;
1546
+ allocateActivityId(): ActivityId$1;
1547
+ createActivity(input: Omit<Extract<DispatchAction, {
1548
+ kind: 'activity-create';
1549
+ }>, 'kind'>, outputUnit?: OutputUnit): {
1550
+ ok: true;
1551
+ activity: Readonly<Activity>;
1552
+ changes: ChangeSet;
1553
+ } | {
1554
+ ok: false;
1555
+ reason: string;
1556
+ };
1557
+ private _enqueue;
1558
+ dispatch(action: DispatchAction, options?: DispatchOptions): Promise<DispatchResult>;
1559
+ private _dispatchInner;
1560
+ /**
1561
+ * Recompute the critical path OUT of band and return a repaintable ChangeSet
1562
+ * of the 7 CP fields, or null when the CP is already fresh for the current
1563
+ * state (coalesced). Enqueued on `_opQueue` so it runs serialized against
1564
+ * dispatches — it reads/writes the live tree only when no mutation is in
1565
+ * flight.
1566
+ *
1567
+ * Deferred-CP flow: dispatch with `{ skipCriticalPath: true }` (paints
1568
+ * without the heavy CP pass), then call this after the paint. Bursts
1569
+ * coalesce: only the first enqueued recompute after the last mutation does
1570
+ * the work; later ones see `_cpComputedEpoch === _mutationEpoch` and no-op.
1571
+ */
1572
+ recomputeCriticalPath(): Promise<{
1573
+ changes: ChangeSet;
1574
+ } | null>;
1575
+ private _recomputeCriticalPathIfStale;
1576
+ private _runCriticalPathAndCapture;
1577
+ undo(options?: {
1578
+ outputUnit?: OutputUnit;
1579
+ }): Promise<ChangeSet | null>;
1580
+ redo(options?: {
1581
+ outputUnit?: OutputUnit;
1582
+ }): Promise<ChangeSet | null>;
1583
+ clearHistory(): void;
1584
+ canUndo(): boolean;
1585
+ canRedo(): boolean;
1586
+ undoDepth(): number;
1587
+ private _resyncCustomIdTrackerFromModel;
1588
+ destroy(): void;
1589
+ private readonly _initPromise;
1590
+ private readonly _criticalPathReady;
1591
+ private readonly _ready;
1592
+ private readonly _saveTracker;
1593
+ private assertReady;
1594
+ /**
1595
+ * Revert the current dispatch's mutations from the write-capture journal. If
1596
+ * the restore ITSELF throws, the state may be a third, partially-inverted
1597
+ * state — worse than either endpoint — so poison the core: refuse all further
1598
+ * dispatches and surface the fault so the host reloads from backend. A failed
1599
+ * rollback is never swallowed.
1600
+ */
1601
+ private _rollback;
1602
+ }
1603
+
1604
+ /**
1605
+ * Parse the raw backend payload into the core's normalized internal model.
1606
+ *
1607
+ * Pure function. No side effects, no state. Throws on structural errors
1608
+ * (missing required fields, broken references) per Q4 in SCHEDULE_CORE_API.md.
1609
+ *
1610
+ * Public from the package — consumers can use this to pre-validate before
1611
+ * instantiating ScheduleCore, or to round-trip data through tests.
1612
+ */
1613
+
1614
+ interface ParsedInput {
1615
+ sector: SectorMetadata;
1616
+ activities: Activity[];
1617
+ links: Link[];
1618
+ calendars: Calendar[];
1619
+ /**
1620
+ * Baseline calendars, kept SEPARATE from the scheduling `calendars`. They
1621
+ * are never used to resolve an activity's calendar — only for the baseline
1622
+ * render. Parsed from the raw `baseCalendars` input (the backend's
1623
+ * `showBaseCalendar`); reshaped here (id+'-base', baseDefault, exceptions
1624
+ * from `baseexceptiondays`) so the core owns the differentiation instead of
1625
+ * the frontend `refreshCalendarsRewrite`.
1626
+ */
1627
+ baseCalendars: Calendar[];
1628
+ }
1629
+ declare function parseFromBackend(input: BackendInput, reporter?: ScheduleCoreReporter): ParsedInput;
1630
+
1631
+ /**
1632
+ * Returns the ACTIVE baseline point of an activity, or null.
1633
+ *
1634
+ * Ported verbatim from the legacy selector used across the schedule
1635
+ * (`react_client/src/views/ganttContainer/gantt/gantt.helper.js`
1636
+ * calculatePonderators / getDurationRecursively, and the baseline columns
1637
+ * `startBaseCol`…`durationBaseCol`):
1638
+ *
1639
+ * activity.baseline_points.find(b => b.sectorbaselineversion?.active)
1640
+ *
1641
+ * Exactly one version per sector is active. Returns null when the activity
1642
+ * has no `baseline_points`, an empty array, or no point whose version is
1643
+ * active — matching the legacy behavior where such activities get
1644
+ * `ponderator = 0` (baseline-only, no fallback to real values; see memory
1645
+ * `project_ponderator_baseline_fix`).
1646
+ *
1647
+ * The `active` flag is treated as truthy (not strictly `=== true`) to match
1648
+ * the legacy `if (base.sectorbaselineversion.active)` check.
1649
+ */
1650
+ declare function getActiveBaseline(activity: {
1651
+ baseline_points?: unknown;
1652
+ } | null | undefined): BaselinePoint | null;
1653
+
1654
+ /**
1655
+ * Surface this function needs from ScheduleState — kept structural so the
1656
+ * function is unit-testable with a plain in-memory adapter.
1657
+ */
1658
+ interface BaselineApplyAdapter {
1659
+ forEachActivity(visit: (a: Record<string, unknown>, id: string) => void): void;
1660
+ getActivity(id: string): Record<string, unknown> | null;
1661
+ setActivityField(id: string, field: string, value: unknown): void;
1662
+ }
1663
+ /**
1664
+ * Overlay baseline points onto activities, matching `point.activityId`
1665
+ * (backend DB id) against `activity.proplannerId`. Writes the full group of
1666
+ * points (all versions) onto `baseline_points`; the active one is selected
1667
+ * downstream by `getActiveBaseline`. Activities absent from the new set get
1668
+ * `baseline_points` reset to `[]`. Returns the ids of activities whose
1669
+ * `baseline_points` changed (so the caller can repaint just those).
1670
+ */
1671
+ declare function applyBaselinePoints(adapter: BaselineApplyAdapter, points: readonly BaselinePoint[]): string[];
1672
+
1673
+ interface CalendarLike {
1674
+ calculateDuration(start: Date, end: Date): number;
1675
+ }
1676
+ /**
1677
+ * Expected % a baseline-planned activity should have completed by `now`,
1678
+ * measured in working time against its baseline calendar. Faithful port of
1679
+ * legacy `calculateExpected` (lookahead-common.js): edge rules first, else the
1680
+ * working-time ratio. Returns 0..100. `now` should already be the comparison
1681
+ * instant the caller wants (e.g. end-of-day).
1682
+ *
1683
+ * Shared by the `expected_progress` (live) and `expected_progress_base` columns
1684
+ * (column-owns-everything: the formula is the one piece both columns share).
1685
+ */
1686
+ declare function expectedProgressFromBaseline(start: Date, end: Date, now: Date, calendar: CalendarLike): number;
1687
+
1688
+ interface ExpectedProgressAdapter {
1689
+ getChildrenIds(parentId: string | 0): readonly string[];
1690
+ getActivity(id: string): Record<string, unknown> | null;
1691
+ setActivityField(id: string, field: string, value: unknown): void;
1692
+ /** Resolve a baseline calendar by its engine id (`${baseCalendarId}-base`). */
1693
+ getBaseCalendar(engineId: string): CalendarLike | null;
1694
+ }
1695
+ /**
1696
+ * Walks the tree from `rootIds`, writing `expected_progress_base` on every
1697
+ * activity: leaves from their baseline calendar, parents as
1698
+ * `Σ(childBase × child.ponderator) / 100` (faithful port of engine.ts
1699
+ * aggregateHierarchy). Returns the ids whose value changed.
1700
+ */
1701
+ declare function computeExpectedProgress(rootIds: readonly string[], now: Date, adapter: ExpectedProgressAdapter, defaultBaseCalendarId?: string | null): string[];
1702
+
1703
+ /**
1704
+ * Recompute the `ponderator` of EVERY activity in the schedule, in bulk.
1705
+ *
1706
+ * Faithful port of `goCalculatePonderators` in
1707
+ * `react_client/src/views/ganttContainer/gantt/index.js:2008-2038` (the
1708
+ * ponderator part — the progress rollup and save are separate concerns the
1709
+ * caller wires). Two passes, mirroring the legacy:
1710
+ * 1. Zero EVERY activity's ponderator.
1711
+ * 2. For each parent (activity with children), redistribute 100 among its
1712
+ * children via `recomputePonderatorsForParent` (baseline-only).
1713
+ *
1714
+ * IMPORTANT — this is the ONLY ponderator recompute the schedule performs.
1715
+ * Verified against the legacy (2026-06-04): the masterplan does NOT recompute
1716
+ * ponderators on add / delete / move / inline-edit. `goCalculatePonderators`
1717
+ * runs ONLY on two events (see index.js:395/443/625):
1718
+ * - the project criterion (`activity_creter`) changed
1719
+ * (`sectorObject.update_ponderators_masterplan`), or
1720
+ * - a baseline was created / saved.
1721
+ * Both events otherwise reload the schedule. So there is intentionally NO
1722
+ * per-mutation cascade and NO dispatch hook — adding one would diverge from
1723
+ * production (a baselined child's siblings stay as the backend computed them
1724
+ * until the next of those two events). See
1725
+ * [[PONDERATOR_BASELINE_MIGRATION_PLAN]].
1726
+ *
1727
+ * Order-independent: a project child's weight derives from descendant
1728
+ * BASELINES (`getDurationRecursively`), never from other ponderators, so the
1729
+ * order parents are visited in does not affect the result.
1730
+ */
1731
+
1732
+ declare function recomputeAllPonderators(criterion: ActivityCreter, adapter: ActivityWriter): void;
1733
+
1734
+ /**
1735
+ * Recompute the weighted-progress rollup of EVERY parent in the schedule,
1736
+ * bottom-up. `parent.progress = Σ(child.progress × child.ponderator / 100)`.
1737
+ *
1738
+ * Faithful port of legacy `check_progress` (editing-flow.js:154-197) — the
1739
+ * second half of `goCalculatePonderators`. After a ponderator recompute
1740
+ * (baseline create/save, see `recomputeAllPonderators`), parent progress is
1741
+ * stale w.r.t. the new weights; this repropagates it.
1742
+ *
1743
+ * PROGRESS-ONLY by design: unlike the dispatch post-processor
1744
+ * `recomputeParentFromChildren` (post-processors/parent-bounds.ts), this does NOT
1745
+ * touch parent start/end/duration — only `progress` and the milestone mirror
1746
+ * `for_disable_milestone_duration`. That keeps it testable in isolation
1747
+ * (no calendars/dates needed) and matches what `check_progress` actually did.
1748
+ *
1749
+ * Order is load-bearing (unlike ponderators, which are order-independent): a
1750
+ * grandparent's rollup consumes its children-parents' ALREADY rolled-up
1751
+ * progress, so parents are visited deepest-first.
1752
+ *
1753
+ * Parity rules:
1754
+ * - child with no/zero ponderator does not contribute;
1755
+ * - a parent whose children contribute nothing keeps its progress frozen;
1756
+ * - `progress` is kept a NUMBER but rounded per level to 2 decimals
1757
+ * (`roundProgressPerLevel`) — legacy `check_progress` recurses on
1758
+ * `toFixed(2)`, so each ancestor consumes its children's 2-decimal value
1759
+ * (MORNING_QUEUE.md — JUN-02).
1760
+ */
1761
+
1762
+ declare function recomputeAllProgressRollup(adapter: ActivityWriter): void;
1763
+
1764
+ /**
1765
+ * Task creation pipeline — types.
1766
+ *
1767
+ * Ports the pure parts of `src/assets/js/custom_actions/createTask/` into a
1768
+ * functional pipeline that replay tests (and eventually prod) can invoke
1769
+ * against an adapter instead of the live gantt.
1770
+ */
1771
+
1772
+ /**
1773
+ * Shape of the task object AFTER the pipeline configures it.
1774
+ *
1775
+ * Mirrors the canonical `Activity` interface (from `../../types.ts`) plus
1776
+ * a handful of Outbuild-specific fields (responsables, tags, subcontractId,
1777
+ * real_constraint_type) and legacy lifecycle flags (new_task, isNewActivity,
1778
+ * $source, $target) that downstream consumers still read.
1779
+ *
1780
+ * Every field in `Activity` is present here. The defaults for the
1781
+ * non-runtime-computed ones live in `NEW_ACTIVITY_DEFAULTS`.
1782
+ */
1783
+ interface NewActivityFields {
1784
+ text: string;
1785
+ description: string;
1786
+ type: 'task' | 'milestone' | 'project';
1787
+ start_date: Date;
1788
+ end_date: Date;
1789
+ duration: number;
1790
+ progress: number;
1791
+ constraint_type: ConstraintType | null;
1792
+ constraint_date: Date | null;
1793
+ calendar_id: CalendarId;
1794
+ /** Parent id (string) or `'0'` for root. Always string — no number drift. */
1795
+ parent: ActivityId | '0';
1796
+ auto_scheduling: boolean;
1797
+ hhWorkTime: number;
1798
+ cost: number;
1799
+ used_cost: number;
1800
+ /**
1801
+ * Earned cost: `(progress/100) × activeBaseline.cost`. A fresh activity has
1802
+ * progress 0 and no active baseline, so it starts at 0 — matching what the
1803
+ * load-path `emitRealCost` would compute for a new leaf. Seeded explicitly so
1804
+ * the field is never `undefined` (which surfaced as `NaN` downstream).
1805
+ */
1806
+ real_cost: number;
1807
+ /** Real worked hours; aggregated from progress submittals. Starts at 0. */
1808
+ real_work: number;
1809
+ /**
1810
+ * Baseline-derived weighting factor used by the parent.progress rollup.
1811
+ * Starts at 0 (no baseline yet); set by the backend on baseline import.
1812
+ * Not editable from the schedule UI — see `project_ponderator_not_editable`.
1813
+ */
1814
+ ponderator: number;
1815
+ responsables: unknown[];
1816
+ tags: unknown[];
1817
+ subcontractId: string | number | null;
1818
+ real_constraint_type: ConstraintType | null;
1819
+ should_be_showed: boolean;
1820
+ /** View-model visibility bit (`visible !== false` = visible). */
1821
+ visible: boolean;
1822
+ activityModifications: unknown[];
1823
+ pendingRequests: unknown[];
1824
+ /** String form, matches Sequelize BIGINT serialization. */
1825
+ unique_correlative_id: string;
1826
+ start_date_backup: string;
1827
+ new_task: true;
1828
+ isNewActivity: true;
1829
+ /**
1830
+ * Tracks outgoing link ids (this activity is the link source).
1831
+ * Initialized empty; mutated by link-create / link-delete to keep the
1832
+ * index consistent. The snapshot reads it directly, so it must always
1833
+ * exist (not undefined) — see D1 in DIVERGENCES_activity_create.md.
1834
+ */
1835
+ $source: string[];
1836
+ /** Same as $source for incoming links (this activity is the target). */
1837
+ $target: string[];
1838
+ /** Pass-through bucket for any extra fields the caller provides. */
1839
+ [key: string]: unknown;
1840
+ }
1841
+
1842
+ /**
1843
+ * Working-day vocabulary — genuine cross-component primitive (creation
1844
+ * defaults, internal/state context fallbacks, link lag display). The value
1845
+ * is the FALLBACK only; a sector- or calendar-provided hoursPerDay always
1846
+ * takes precedence at every consumer.
1847
+ */
1848
+ declare const DEFAULT_HOURS_PER_DAY = 8;
1849
+
1850
+ /**
1851
+ * Defaults for newly-created activities — the **canonical template** that
1852
+ * `buildNewActivity` spreads before applying caller-provided overrides.
1853
+ *
1854
+ * Single source of truth: any documentation that says "a new activity
1855
+ * starts as X" must reference this constant, never inline literals.
1856
+ *
1857
+ * The shape produced by spreading this constant + the runtime-computed
1858
+ * fields below is a complete Activity (every field in the `Activity`
1859
+ * interface from `../../types.ts` is accounted for).
1860
+ *
1861
+ * Fields NOT in this constant are computed at runtime because they depend
1862
+ * on input or context:
1863
+ * - `id` → allocated by `ActivityIdGenerator.next()`
1864
+ * - `parent` → from `DispatchAction.parentId` (or `'0'`)
1865
+ * - `start_date` → inherited from parent or `fallbackStartDate`,
1866
+ * then snapped to the calendar's next
1867
+ * working-time boundary
1868
+ * - `end_date` → calendar-aware via `adapter.calculateEndDate`
1869
+ * - `duration` → override > `sector.hoursPerDay` > `8`
1870
+ * - `calendar_id` → override > `sector.defaultCalendar`
1871
+ * - `unique_correlative_id` → allocated by `UniqueCorrelativeIdGenerator.next()`
1872
+ * - `custom_id` → allocated by `customIdTracker`
1873
+ * - `text` → `'New Master Plan'` when this is the very
1874
+ * first activity of the sector, `'New Activity'`
1875
+ * otherwise (override beats both)
1876
+ * - `correlative_id` → assigned by `recomputeCorrelativeIds` after
1877
+ * the new activity has been inserted in the
1878
+ * parent's children list
1879
+ *
1880
+ * The `real_constraint_type` field is set AFTER the spread of overrides so
1881
+ * it always mirrors the (possibly overridden) `constraint_type`.
1882
+ */
1883
+
1884
+ declare const NEW_ACTIVITY_DEFAULTS: Readonly<Omit<NewActivityFields, 'id' | 'parent' | 'start_date' | 'end_date' | 'duration' | 'calendar_id' | 'unique_correlative_id' | 'text' | 'start_date_backup'>>;
1885
+
1886
+ /**
1887
+ * Schedule-core equivalent of the legacy frontend `checkNoUpdatedLinks`.
1888
+ *
1889
+ * Returns the CURRENT links that were MODIFIED relative to a `baseline` (the
1890
+ * last loaded/saved link state). A link is "modified" iff its `lag` or `type`
1891
+ * differs from its baseline counterpart (matched by `id`).
1892
+ *
1893
+ * Why only lag/type: empirically those are the only mutable fields of a link —
1894
+ * `source`/`target` are identity, and the rest are backend/derived. The core
1895
+ * `link-update` intent (`dispatch/types.ts`) changes exactly "lag and/or type",
1896
+ * so this comparison is complete.
1897
+ *
1898
+ * New links (absent from baseline) and deleted links (present only in baseline)
1899
+ * are NOT "modified" — those belong to the unsaved / deleted buckets. The
1900
+ * baseline is supplied by the caller (the save flow's reference snapshot); per
1901
+ * interface discipline this is a pure helper, not a method on the core/port.
1902
+ */
1903
+ declare function checkNoUpdatedLinks(baseline: readonly Link[], current: readonly Link[]): Link[];
1904
+
1905
+ /**
1906
+ * Schedule-core equivalent of the legacy frontend `checkNoSavedActivities` /
1907
+ * `checkNoSavedLinks`: an entity is "unsaved" when it has no backend id
1908
+ * (`proplannerId`) — i.e. it was created in-session and not yet persisted.
1909
+ *
1910
+ * Pure helper (same `!proplannerId` criterion as legacy, so 0/null/undefined
1911
+ * all count as unsaved). Works on `core.getAllActivitiesView()` / `getAllLinks()`
1912
+ * or on `gantt.serialize().data` — both carry `proplannerId`. Per interface
1913
+ * discipline this is a helper, not a method on the core/port.
1914
+ */
1915
+ interface WithProplannerId {
1916
+ proplannerId?: number | null;
1917
+ [key: string]: unknown;
1918
+ }
1919
+ declare const getUnsavedActivities: <T extends WithProplannerId>(activities?: readonly T[]) => T[];
1920
+
1921
+ /**
1922
+ * Returns a microtask-priority yield. Resolves on the next event-loop
1923
+ * tick — the browser is free to paint, handle input, or process other
1924
+ * tasks in between.
1925
+ */
1926
+ declare function yieldToBrowser(): Promise<void>;
1927
+
1928
+ /**
1929
+ * Does this dispatch recompute the critical path? The single authority for the
1930
+ * question, shared by the dispatch gate (`resolveDerivedPasses`) and the
1931
+ * bridge, which reads it to defer the CP out of the paint. Mirrors legacy: the
1932
+ * CP re-derives on any scheduling change, and is left STALE for prop-only
1933
+ * intents, the structural reparents (indent/outdent), and edits that touch only
1934
+ * non-scheduling columns.
1935
+ */
1936
+ declare function willRunCriticalPath(action: DispatchAction): boolean;
1937
+
1938
+ /** Activity types */
1939
+ declare const ACTIVITY_TYPE: {
1940
+ readonly TASK: "task";
1941
+ readonly PROJECT: "project";
1942
+ readonly MILESTONE: "milestone";
1943
+ };
1944
+
1945
+ /**
1946
+ * Root-parent sentinel — single source of truth.
1947
+ *
1948
+ * Root-level activities have meant "root" under several shapes across the
1949
+ * codebase and the recorded data: string `'0'` (post-parse domain model),
1950
+ * numeric `0` (DHTMLX / engine seam), and nullish. `isRootParent` accepts
1951
+ * all of them; `normalizeParentKey` collapses them to the canonical `'0'`.
1952
+ *
1953
+ * NOTE: `'0'` is overloaded elsewhere in the domain (FS link-type code,
1954
+ * correlative ids). These constants are ONLY for the parent axis.
1955
+ */
1956
+ /** Canonical root-parent id in the post-parse domain model. */
1957
+ declare const ROOT_PARENT_ID: "0";
1958
+ /** True when `parent` denotes the root level, under any historical shape. */
1959
+ declare function isRootParent(parent: unknown): boolean;
1960
+ /** Collapse any root shape to the canonical `'0'`; stringify the rest. */
1961
+ declare function normalizeParentKey(parent: unknown): string;
1962
+
1963
+ /**
1964
+ * Canonical bidirectional map between DHTMLX link kinds ('0'-'3') and the
1965
+ * backend/display two-letter codes (fs/ss/ff/sf), plus the predecessor
1966
+ * display-string vocabulary.
1967
+ *
1968
+ * Single source of truth — replaces the hand-maintained copies that lived in
1969
+ * parse-predecessor-string (code→number), apply-link-operation
1970
+ * (number→code), boundary/backend parseFromBackend (code→number) and
1971
+ * inverses (number→code). The mutual `satisfies Record<…>` constraints make
1972
+ * omitting or typo'ing a key on either side a compile error.
1973
+ */
1974
+
1975
+ /** Backend/display two-letter link codes. */
1976
+ type LinkTypeCode = 'fs' | 'ss' | 'ff' | 'sf';
1977
+ /** DHTMLX numeric kind → backend/display code. */
1978
+ declare const LINK_TYPE_CODE: {
1979
+ readonly "0": "fs";
1980
+ readonly "1": "ss";
1981
+ readonly "2": "ff";
1982
+ readonly "3": "sf";
1983
+ };
1984
+ /** Backend/display code → DHTMLX numeric kind (inverse of LINK_TYPE_CODE). */
1985
+ declare const LINK_CODE_TO_TYPE: {
1986
+ readonly fs: "0";
1987
+ readonly ss: "1";
1988
+ readonly ff: "2";
1989
+ readonly sf: "3";
1990
+ };
1991
+
1992
+ /**
1993
+ * Inverse boundary maps (domain → backend vocabulary).
1994
+ *
1995
+ * The forward translations live in `parseFromBackend.ts`
1996
+ * (`CONSTRAINT_LABEL_MAP`, `LINK_CODE_TO_NUMBER`). The *serialization*
1997
+ * core → backend is intentionally NOT owned by this package today — it
1998
+ * lives in the consumer bridge (`react_client/gantt.helper.js`,
1999
+ * `from_number_to_code` / `GanttConstraint` reverse lookup) because the
2000
+ * bridge is the source of truth for what the server expects on save.
2001
+ *
2002
+ * These inverses exist so the mapping is documented and testable next to
2003
+ * its forward counterpart, and so a future `core → backend` layer doesn't
2004
+ * re-derive them by hand. **Internal only — not exported in `src/index.ts`.**
2005
+ * See Obsidian `BOUNDARY_TRANSLATIONS.md` §"Mapeos inversos".
2006
+ */
2007
+
2008
+ /** Inverse of `CONSTRAINT_LABEL_MAP` — internal code → backend label. */
2009
+ declare const CONSTRAINT_TYPE_TO_LABEL: Record<ConstraintType, string>;
2010
+
2011
+ /**
2012
+ * Canonical catalog of `DispatchAction` kinds.
2013
+ *
2014
+ * The discriminated union in `types.ts` remains the source of truth; this
2015
+ * module exposes it as RUNTIME vocabulary so `.js` consumers (bridge
2016
+ * entry-points, guard tests) stop retyping the wire strings. Exhaustiveness
2017
+ * is compile-checked HERE (not in the guard test — `tsconfig.json` excludes
2018
+ * `*.test.ts` from `tsc --noEmit` and vitest does not typecheck):
2019
+ * - `satisfies Record<string, DispatchActionKind>` rejects any entry whose
2020
+ * value is not a union member (catalog ⊆ union);
2021
+ * - `KIND_CATALOG_COVERS_UNION: Record<DispatchActionKind, true>` requires
2022
+ * one entry per union member (union ⊆ catalog) — adding a kind to
2023
+ * `DispatchAction` without updating this module breaks the build.
2024
+ * The string VALUES are a frozen contract (see `constants.guard.test.ts`).
2025
+ */
2026
+
2027
+ type DispatchActionKind = DispatchAction['kind'];
2028
+ declare const DISPATCH_ACTION_KIND: {
2029
+ readonly INLINE_EDIT: "inline-edit";
2030
+ readonly DATES_BATCH: "dates-batch";
2031
+ readonly BULK_EDIT: "bulk-edit";
2032
+ readonly LINK_CREATE: "link-create";
2033
+ readonly LINK_UPDATE: "link-update";
2034
+ readonly LINK_DELETE: "link-delete";
2035
+ readonly LINKS_BATCH: "links-batch";
2036
+ readonly ACTIVITY_CREATE: "activity-create";
2037
+ readonly ACTIVITY_DELETE: "activity-delete";
2038
+ readonly ACTIVITY_MOVE: "activity-move";
2039
+ readonly ACTIVITY_INDENT: "activity-indent";
2040
+ readonly ACTIVITY_OUTDENT: "activity-outdent";
2041
+ readonly ACTIVITY_SET_PROGRESS: "activity-set-progress";
2042
+ readonly ACTIVITY_PASTE: "activity-paste";
2043
+ readonly SELECTION_TOGGLE: "selection-toggle";
2044
+ readonly SELECTION_REPLACE: "selection-replace";
2045
+ readonly VISIBILITY_SET: "visibility-set";
2046
+ readonly SIR_SYNC: "sir-sync";
2047
+ };
2048
+ declare const DISPATCH_ACTION_KINDS: ReadonlyArray<DispatchActionKind>;
2049
+
2050
+ /**
2051
+ * Canonical vocabulary of activity-creation triggers.
2052
+ *
2053
+ * The engine's `activity-create` intent does NOT carry this field — these
2054
+ * kinds name the USER trigger the consumer's creation hub routes on before
2055
+ * dispatching (`requestActivityCreation` / `CreateActivityIntent` in
2056
+ * `react_client` core path). The engine owns the vocabulary so the client
2057
+ * and the bridge import ONE canonical source instead of retyping the
2058
+ * literals (CONSTANTES_DECISIONES paso 1). The string VALUES are a frozen
2059
+ * contract — verified verbatim against the client union
2060
+ * (`core/features/createActivity/intents.ts`) on 2026-07-02.
2061
+ *
2062
+ * - `CHILD` — per-row "+" button (runs the lookahead guard).
2063
+ * - `CHILD_CONFIRMED` — lookahead-cleanup modal confirm (guard already passed).
2064
+ * - `LINE` — green bar "add-task-by-line".
2065
+ * - `ROOT` — generic "+" (header / create), no guard.
2066
+ */
2067
+ declare const CREATION_KIND: {
2068
+ readonly CHILD: "child";
2069
+ readonly CHILD_CONFIRMED: "child-confirmed";
2070
+ readonly LINE: "line";
2071
+ readonly ROOT: "root";
2072
+ };
2073
+ type CreationKind = (typeof CREATION_KIND)[keyof typeof CREATION_KIND];
2074
+
2075
+ /**
2076
+ * Canonical catalog of dispatch rejection reasons.
2077
+ *
2078
+ * `DispatchResult.reason` stays `string` on purpose: it is the cross-domain
2079
+ * bag that also carries pipeline gate codes (`ParseErrorCode`,
2080
+ * `ValidationReason`, `EditDenialReason`) and the link-engine rejected
2081
+ * verdicts. This module covers the reasons DISPATCH ITSELF produces, so
2082
+ * producers and tests share one compile-checked source. The string VALUES
2083
+ * are a frozen contract — the bridge matches on them.
2084
+ */
2085
+ declare const REJECTION_REASON: {
2086
+ readonly CANNOT_EDIT: "cannot_edit";
2087
+ readonly PARSE_ERROR: "parse_error";
2088
+ readonly INVALID: "invalid";
2089
+ readonly CUSTOM_ID_DUPLICATE: "custom_id_duplicate";
2090
+ readonly INVALID_LINK_TYPE: "invalid_link_type";
2091
+ readonly INVALID_LINK_LAG: "invalid_link_lag";
2092
+ readonly INVALID_VALUE: "invalid_value";
2093
+ readonly NO_PIPELINE_FOR_COLUMN: "no_pipeline_for_column";
2094
+ readonly ACTIVITY_NOT_FOUND: "activity_not_found";
2095
+ readonly PARENT_NOT_FOUND: "parent_not_found";
2096
+ readonly ACTIVITY_IDS_EMPTY: "activity_ids_empty";
2097
+ readonly ANCHOR_SIBLING_CONFLICT: "anchor_sibling_conflict";
2098
+ readonly ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found";
2099
+ readonly ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent";
2100
+ readonly PARENT_FROZEN_BY_SIR: "parent_frozen_by_sir";
2101
+ readonly CANNOT_MOVE_INTO_OWN_DESCENDANT: "cannot_move_into_own_descendant";
2102
+ readonly INDENT_NO_ELIGIBLE_SIBLING: "indent_no_eligible_sibling";
2103
+ readonly OUTDENT_ROOT_LEVEL_NOT_EDITABLE: "outdent_root_level_not_editable";
2104
+ readonly INVALID_PROGRESS_VALUE: "invalid_progress_value_must_be_0_or_100";
2105
+ };
2106
+ type DispatchRejectReason = (typeof REJECTION_REASON)[keyof typeof REJECTION_REASON];
2107
+
2108
+ /**
2109
+ * Amplitude event names emitted by the dispatch layer (structure mutations).
2110
+ * Analytics CONTRACT — values must stay byte-identical to production.
2111
+ * Column-level entry events live in `columns/shared/trackingEvents.ts`.
2112
+ */
2113
+ declare const DISPATCH_TRACK_EVENT: {
2114
+ readonly ACTIVITY_CREATION: "schedule_activity_creation";
2115
+ readonly ACTIVITY_DELETION: "schedule_activity_deletion";
2116
+ readonly ACTIVITY_MOVE: "schedule_activity_move";
2117
+ readonly ACTIVITY_INDENT: "schedule_activity_indent";
2118
+ readonly ACTIVITY_OUTDENT: "schedule_activity_outdent";
2119
+ };
2120
+ type DispatchTrackEvent = (typeof DISPATCH_TRACK_EVENT)[keyof typeof DISPATCH_TRACK_EVENT];
2121
+
2122
+ export { ACTIVITY_TYPE, type Activity, type ActivityCreter, type ActivityId$1 as ActivityId, type ActivityType, type BackendActivityInput, type BackendCalendar, type BackendCalendarException, type BackendInput, type BackendLink, type BackendScheduleImpactRequests, type BackendSector, type BackendShift, type BaselinePoint, type BaselineVersion, CALENDAR_UNIT, COLUMN, CONSTRAINT_TYPE, CONSTRAINT_TYPE_TO_LABEL, CREATION_KIND, type Calendar, type CalendarId$1 as CalendarId, type CalendarUnit, type CalendarWorktime, type ChangeSet, type ColumnName, type ConstraintType, type ConstraintWarning, type CreationKind, DEFAULT_HOURS_PER_DAY, DISPATCH_ACTION_KIND, DISPATCH_ACTION_KINDS, DISPATCH_TRACK_EVENT, type DispatchAction, type DispatchActionKind, type DispatchOptions, type DispatchRejectReason, type DispatchResult, type DispatchTrackEvent, type EntityChange, LINK_CODE_TO_TYPE, LINK_TYPE, LINK_TYPE_CODE, type Link, type LinkId, type LinkType, type LinkTypeCode, type LinksBatchOperation, NEW_ACTIVITY_DEFAULTS, type OutputUnit, type ParsedInput, type PastedActivityInput, type PastedLinkInput, type PendingRequest, REJECTION_REASON, ROOT_PARENT_ID, ScheduleCore, type ScheduleCoreInput, type ScheduleCoreReporter, type ScheduleCoreStatus, type ScheduleEffect, type SectorMetadata, type StatusCriteria, type TrackingEvent, WORK_TIME_DIRECTION, type WorkTimeDirection, applyBaselinePoints, checkNoUpdatedLinks, computeExpectedProgress, expectedProgressFromBaseline, getActiveBaseline, getUnsavedActivities, isRootParent, normalizeParentKey, parseFromBackend, recomputeAllPonderators, recomputeAllProgressRollup, willRunCriticalPath, yieldToBrowser };