gantt-lib 0.127.0 → 0.128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/scheduling/index.d.mts +1 -1
- package/dist/core/scheduling/index.d.ts +1 -1
- package/dist/core/scheduling/index.js +1123 -0
- package/dist/core/scheduling/index.js.map +1 -1
- package/dist/core/scheduling/index.mjs +1122 -0
- package/dist/core/scheduling/index.mjs.map +1 -1
- package/dist/{index-B1YUnlWS.d.mts → index-BWHqH6v8.d.mts} +86 -1
- package/dist/{index-B1YUnlWS.d.ts → index-BWHqH6v8.d.ts} +86 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1270 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1269 -11
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +27 -2
- package/package.json +1 -1
|
@@ -1357,6 +1357,1127 @@ function validateDependencies(tasks) {
|
|
|
1357
1357
|
errors
|
|
1358
1358
|
};
|
|
1359
1359
|
}
|
|
1360
|
+
|
|
1361
|
+
// src/core/scheduling/subtreeScaling.ts
|
|
1362
|
+
function toIsoDate2(date) {
|
|
1363
|
+
return date.toISOString().split("T")[0];
|
|
1364
|
+
}
|
|
1365
|
+
function isoOfTaskDate(date) {
|
|
1366
|
+
return toIsoDate2(parseDateOnly(date));
|
|
1367
|
+
}
|
|
1368
|
+
function scaleFailure(code, details) {
|
|
1369
|
+
return details && details.length > 0 ? { ok: false, code, changedTasks: [], changedIds: [], details } : { ok: false, code, changedTasks: [], changedIds: [] };
|
|
1370
|
+
}
|
|
1371
|
+
function isFiniteInteger(value) {
|
|
1372
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
1373
|
+
}
|
|
1374
|
+
function dependencyLag(dep) {
|
|
1375
|
+
return Number.isFinite(dep.lag) ? dep.lag : 0;
|
|
1376
|
+
}
|
|
1377
|
+
function createSolveSpace(mirrored, anchorDate, businessDays, weekendPredicate) {
|
|
1378
|
+
const space = {
|
|
1379
|
+
mirrored,
|
|
1380
|
+
anchorDate,
|
|
1381
|
+
businessDays,
|
|
1382
|
+
weekendPredicate,
|
|
1383
|
+
idxCache: /* @__PURE__ */ new Map(),
|
|
1384
|
+
idxOf(date) {
|
|
1385
|
+
const key = toIsoDate2(date);
|
|
1386
|
+
const cached = space.idxCache.get(key);
|
|
1387
|
+
if (cached !== void 0) {
|
|
1388
|
+
return cached;
|
|
1389
|
+
}
|
|
1390
|
+
let idx;
|
|
1391
|
+
if (businessDays && weekendPredicate) {
|
|
1392
|
+
idx = mirrored ? getBusinessDayOffset(date, anchorDate, weekendPredicate) : getBusinessDayOffset(anchorDate, date, weekendPredicate);
|
|
1393
|
+
} else {
|
|
1394
|
+
const deltaMs = date.getTime() - anchorDate.getTime();
|
|
1395
|
+
idx = mirrored ? -Math.round(deltaMs / DAY_MS) : Math.round(deltaMs / DAY_MS);
|
|
1396
|
+
}
|
|
1397
|
+
space.idxCache.set(key, idx);
|
|
1398
|
+
return idx;
|
|
1399
|
+
},
|
|
1400
|
+
dateOf(idx) {
|
|
1401
|
+
if (businessDays && weekendPredicate) {
|
|
1402
|
+
return shiftBusinessDayOffset(anchorDate, mirrored ? -idx : idx, weekendPredicate);
|
|
1403
|
+
}
|
|
1404
|
+
const delta = mirrored ? -idx : idx;
|
|
1405
|
+
return new Date(anchorDate.getTime() + delta * DAY_MS);
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
return space;
|
|
1409
|
+
}
|
|
1410
|
+
function validateScaleInput(parentId, targetDuration, snapshot, businessDays, weekendPredicate) {
|
|
1411
|
+
if (!isFiniteInteger(targetDuration) || targetDuration < 1) {
|
|
1412
|
+
return { ok: false, code: "INVALID_TARGET_DURATION", details: [`targetDuration=${String(targetDuration)}`] };
|
|
1413
|
+
}
|
|
1414
|
+
const taskById = /* @__PURE__ */ new Map();
|
|
1415
|
+
const duplicateIds = [];
|
|
1416
|
+
for (const task of snapshot) {
|
|
1417
|
+
if (taskById.has(task.id)) duplicateIds.push(task.id);
|
|
1418
|
+
taskById.set(task.id, task);
|
|
1419
|
+
}
|
|
1420
|
+
if (duplicateIds.length > 0) {
|
|
1421
|
+
return { ok: false, code: "INVALID_HIERARCHY", details: [`duplicate task ids: ${duplicateIds.slice(0, 5).join(", ")}`] };
|
|
1422
|
+
}
|
|
1423
|
+
const parentTask = taskById.get(parentId);
|
|
1424
|
+
if (!parentTask) {
|
|
1425
|
+
return { ok: false, code: "TASK_NOT_FOUND", details: [`parentId=${parentId}`] };
|
|
1426
|
+
}
|
|
1427
|
+
const hasChild = snapshot.some((task) => task.parentId === parentId);
|
|
1428
|
+
if (!hasChild) {
|
|
1429
|
+
return { ok: false, code: "NOT_A_PARENT", details: [`task ${parentId} has no children`] };
|
|
1430
|
+
}
|
|
1431
|
+
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
1432
|
+
const hierarchyErrors = [];
|
|
1433
|
+
for (const task of snapshot) {
|
|
1434
|
+
if (!task.parentId) continue;
|
|
1435
|
+
if (task.parentId === task.id) {
|
|
1436
|
+
hierarchyErrors.push(`self-parent: ${task.id}`);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (!taskById.has(task.parentId)) {
|
|
1440
|
+
hierarchyErrors.push(`missing parent ${task.parentId} for ${task.id}`);
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
const children = childrenByParentId.get(task.parentId) ?? [];
|
|
1444
|
+
children.push(task);
|
|
1445
|
+
childrenByParentId.set(task.parentId, children);
|
|
1446
|
+
}
|
|
1447
|
+
if (hierarchyErrors.length === 0) {
|
|
1448
|
+
const globallyVisited = /* @__PURE__ */ new Set();
|
|
1449
|
+
for (const task of snapshot) {
|
|
1450
|
+
if (globallyVisited.has(task.id)) continue;
|
|
1451
|
+
const chain = /* @__PURE__ */ new Set();
|
|
1452
|
+
let current = task;
|
|
1453
|
+
while (current?.parentId) {
|
|
1454
|
+
if (chain.has(current.id)) {
|
|
1455
|
+
hierarchyErrors.push(`hierarchy cycle at ${current.id}`);
|
|
1456
|
+
break;
|
|
1457
|
+
}
|
|
1458
|
+
chain.add(current.id);
|
|
1459
|
+
globallyVisited.add(current.id);
|
|
1460
|
+
current = taskById.get(current.parentId);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (hierarchyErrors.length > 0) {
|
|
1465
|
+
return { ok: false, code: "INVALID_HIERARCHY", details: hierarchyErrors.slice(0, 5) };
|
|
1466
|
+
}
|
|
1467
|
+
const subtreeIds = collectSubtreeIds(parentId, childrenByParentId);
|
|
1468
|
+
const participating = new Set(subtreeIds);
|
|
1469
|
+
participating.add(parentId);
|
|
1470
|
+
for (const task of snapshot) {
|
|
1471
|
+
const isInternal = subtreeIds.has(task.id) || task.id === parentId;
|
|
1472
|
+
for (const dep of task.dependencies ?? []) {
|
|
1473
|
+
if (isInternal) participating.add(dep.taskId);
|
|
1474
|
+
if (subtreeIds.has(dep.taskId) || dep.taskId === parentId) participating.add(task.id);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const dateErrors = [];
|
|
1478
|
+
for (const id of participating) {
|
|
1479
|
+
const task = taskById.get(id);
|
|
1480
|
+
if (!task) continue;
|
|
1481
|
+
const start = parseDateOnly(task.startDate);
|
|
1482
|
+
const end = parseDateOnly(task.endDate);
|
|
1483
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime())) {
|
|
1484
|
+
dateErrors.push(`unparseable dates on ${id}`);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (end.getTime() < start.getTime()) {
|
|
1488
|
+
dateErrors.push(`endDate before startDate on ${id}`);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (!subtreeIds.has(id)) continue;
|
|
1492
|
+
if (task.type === "milestone" && start.getTime() !== end.getTime()) {
|
|
1493
|
+
dateErrors.push(`milestone ${id} must have equal start and end dates`);
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (businessDays && weekendPredicate && !childrenByParentId.has(task.id)) {
|
|
1497
|
+
if (weekendPredicate(start) || weekendPredicate(end)) {
|
|
1498
|
+
dateErrors.push(`leaf ${id} boundary falls on a weekend in business-day mode`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
if (dateErrors.length > 0) {
|
|
1503
|
+
return { ok: false, code: "INVALID_DATES", details: dateErrors.slice(0, 5) };
|
|
1504
|
+
}
|
|
1505
|
+
const depValidation = validateDependencies(snapshot);
|
|
1506
|
+
if (!depValidation.isValid) {
|
|
1507
|
+
return {
|
|
1508
|
+
ok: false,
|
|
1509
|
+
code: "INVALID_DEPENDENCIES",
|
|
1510
|
+
details: depValidation.errors.slice(0, 5).map((error) => `${error.type}: ${error.taskId}`)
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
return { ok: true, parentTask };
|
|
1514
|
+
}
|
|
1515
|
+
function collectSubtreeIds(parentId, childrenByParentId) {
|
|
1516
|
+
const subtreeIds = /* @__PURE__ */ new Set();
|
|
1517
|
+
const queue = [parentId];
|
|
1518
|
+
while (queue.length > 0) {
|
|
1519
|
+
const current = queue.shift();
|
|
1520
|
+
for (const child of childrenByParentId.get(current) ?? []) {
|
|
1521
|
+
if (subtreeIds.has(child.id)) continue;
|
|
1522
|
+
subtreeIds.add(child.id);
|
|
1523
|
+
queue.push(child.id);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return subtreeIds;
|
|
1527
|
+
}
|
|
1528
|
+
var PARENT_BOUND_KEY = "__parent_bound__";
|
|
1529
|
+
function buildScaleContext(parentTask, snapshot, options) {
|
|
1530
|
+
const businessDays = options.businessDays ?? false;
|
|
1531
|
+
const weekendPredicate = options.weekendPredicate;
|
|
1532
|
+
const isImmutable = options.isImmutable ?? ((task) => task.locked === true);
|
|
1533
|
+
const indexedSnapshot = snapshot.map((task, index) => ({ task, index }));
|
|
1534
|
+
const taskById = new Map(indexedSnapshot.map(({ task }) => [task.id, task]));
|
|
1535
|
+
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
1536
|
+
for (const task of snapshot) {
|
|
1537
|
+
if (!task.parentId) continue;
|
|
1538
|
+
const children = childrenByParentId.get(task.parentId) ?? [];
|
|
1539
|
+
children.push(task);
|
|
1540
|
+
childrenByParentId.set(task.parentId, children);
|
|
1541
|
+
}
|
|
1542
|
+
const subtreeIds = collectSubtreeIds(parentTask.id, childrenByParentId);
|
|
1543
|
+
const depthById = /* @__PURE__ */ new Map([[parentTask.id, 0]]);
|
|
1544
|
+
const depthQueue = [parentTask.id];
|
|
1545
|
+
while (depthQueue.length > 0) {
|
|
1546
|
+
const current = depthQueue.shift();
|
|
1547
|
+
const depth = depthById.get(current) ?? 0;
|
|
1548
|
+
for (const child of childrenByParentId.get(current) ?? []) {
|
|
1549
|
+
depthById.set(child.id, depth + 1);
|
|
1550
|
+
depthQueue.push(child.id);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const subtreeTasks = indexedSnapshot.filter(({ task }) => subtreeIds.has(task.id));
|
|
1554
|
+
const leavesRaw = subtreeTasks.filter(({ task }) => !childrenByParentId.has(task.id));
|
|
1555
|
+
const nestedParentsRaw = subtreeTasks.filter(
|
|
1556
|
+
({ task }) => childrenByParentId.has(task.id) && task.id !== parentTask.id
|
|
1557
|
+
);
|
|
1558
|
+
const leafEntries = leavesRaw.map(({ task, index }) => {
|
|
1559
|
+
const start = parseDateOnly(task.startDate);
|
|
1560
|
+
const end = parseDateOnly(task.endDate);
|
|
1561
|
+
return { task, index, start, end };
|
|
1562
|
+
});
|
|
1563
|
+
const computedStart = new Date(Math.min(...leafEntries.map((entry) => entry.start.getTime())));
|
|
1564
|
+
const computedEnd = new Date(Math.max(...leafEntries.map((entry) => entry.end.getTime())));
|
|
1565
|
+
const mirrored = (options.anchor ?? "start") === "end";
|
|
1566
|
+
const anchorDate = mirrored ? computedEnd : computedStart;
|
|
1567
|
+
const space = createSolveSpace(mirrored, anchorDate, businessDays, weekendPredicate);
|
|
1568
|
+
let lo = Number.POSITIVE_INFINITY;
|
|
1569
|
+
let hi = Number.NEGATIVE_INFINITY;
|
|
1570
|
+
for (const entry of leafEntries) {
|
|
1571
|
+
lo = Math.min(lo, space.idxOf(entry.start), space.idxOf(entry.end));
|
|
1572
|
+
hi = Math.max(hi, space.idxOf(entry.start), space.idxOf(entry.end));
|
|
1573
|
+
}
|
|
1574
|
+
const originalSpan = Number.isFinite(lo) ? hi - lo + 1 : 1;
|
|
1575
|
+
const immutableById = /* @__PURE__ */ new Map();
|
|
1576
|
+
for (const { task } of subtreeTasks) {
|
|
1577
|
+
immutableById.set(task.id, isImmutable(task) === true);
|
|
1578
|
+
}
|
|
1579
|
+
const getMinDuration = options.getMinDuration;
|
|
1580
|
+
const minDurationErrors = [];
|
|
1581
|
+
const minDurationById = /* @__PURE__ */ new Map();
|
|
1582
|
+
for (const { task } of subtreeTasks) {
|
|
1583
|
+
if (childrenByParentId.has(task.id)) continue;
|
|
1584
|
+
if (task.type === "milestone") {
|
|
1585
|
+
minDurationById.set(task.id, 0);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
let min = 1;
|
|
1589
|
+
if (getMinDuration) {
|
|
1590
|
+
const requested = getMinDuration(task);
|
|
1591
|
+
if (!isFiniteInteger(requested) || requested < 0) {
|
|
1592
|
+
minDurationErrors.push(`getMinDuration(${task.id}) = ${String(requested)}`);
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
min = Math.max(1, requested);
|
|
1596
|
+
}
|
|
1597
|
+
minDurationById.set(task.id, min);
|
|
1598
|
+
}
|
|
1599
|
+
if (minDurationErrors.length > 0) {
|
|
1600
|
+
return { ok: false, code: "INVALID_MINIMUM", details: minDurationErrors.slice(0, 5) };
|
|
1601
|
+
}
|
|
1602
|
+
const leaves = leafEntries.map(({ task, index, start, end }) => ({
|
|
1603
|
+
id: task.id,
|
|
1604
|
+
task,
|
|
1605
|
+
snapshotIndex: index,
|
|
1606
|
+
depth: depthById.get(task.id) ?? 1,
|
|
1607
|
+
isMilestone: task.type === "milestone",
|
|
1608
|
+
immutable: immutableById.get(task.id) === true,
|
|
1609
|
+
startIdx: space.idxOf(start),
|
|
1610
|
+
endIdx: space.idxOf(end),
|
|
1611
|
+
duration: getTaskDuration(start, end, businessDays, weekendPredicate),
|
|
1612
|
+
minDuration: minDurationById.get(task.id) ?? 1,
|
|
1613
|
+
componentId: -1,
|
|
1614
|
+
isRoot: false
|
|
1615
|
+
}));
|
|
1616
|
+
const leafById = new Map(leaves.map((leaf) => [leaf.id, leaf]));
|
|
1617
|
+
const anchorLeafByParent = /* @__PURE__ */ new Map();
|
|
1618
|
+
const nestedParents = nestedParentsRaw.map(({ task, index }) => ({ task, snapshotIndex: index, depth: depthById.get(task.id) ?? 1 })).sort((left, right) => right.depth - left.depth || left.snapshotIndex - right.snapshotIndex);
|
|
1619
|
+
for (const { task } of nestedParents) {
|
|
1620
|
+
let earliestStart = null;
|
|
1621
|
+
let latestEnd = null;
|
|
1622
|
+
for (const leaf of leaves) {
|
|
1623
|
+
if (!isDescendantOf(leaf.task, task.id, taskById)) continue;
|
|
1624
|
+
if (!earliestStart || leaf.startIdx < earliestStart.startIdx) earliestStart = leaf;
|
|
1625
|
+
if (!latestEnd || leaf.endIdx > latestEnd.endIdx) latestEnd = leaf;
|
|
1626
|
+
}
|
|
1627
|
+
if (earliestStart && latestEnd) {
|
|
1628
|
+
anchorLeafByParent.set(task.id, { earliestStartId: earliestStart.id, latestEndId: latestEnd.id });
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
const getMinLag = options.getMinLag;
|
|
1632
|
+
const edges = [];
|
|
1633
|
+
const boundsByLeaf = /* @__PURE__ */ new Map();
|
|
1634
|
+
const outgoingRefs = [];
|
|
1635
|
+
const lagErrors = [];
|
|
1636
|
+
const pushEdge = (ownerTask, dep, depIndex, predLeafId, succLeafId) => {
|
|
1637
|
+
const originalLag = dependencyLag(dep);
|
|
1638
|
+
const succLeaf = leafById.get(succLeafId);
|
|
1639
|
+
const adjustable = originalLag > 0 && !!succLeaf && !succLeaf.immutable && !immutableById.get(ownerTask.id);
|
|
1640
|
+
let minLag = originalLag;
|
|
1641
|
+
if (adjustable) {
|
|
1642
|
+
minLag = 0;
|
|
1643
|
+
if (getMinLag) {
|
|
1644
|
+
const requested = getMinLag(dep, ownerTask);
|
|
1645
|
+
if (!isFiniteInteger(requested) || requested < 0 || requested > originalLag) {
|
|
1646
|
+
lagErrors.push(`getMinLag(${ownerTask.id}#${depIndex}) = ${String(requested)}`);
|
|
1647
|
+
} else {
|
|
1648
|
+
minLag = requested;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
edges.push({
|
|
1653
|
+
key: `${ownerTask.id}#${depIndex}`,
|
|
1654
|
+
predLeafId,
|
|
1655
|
+
succLeafId,
|
|
1656
|
+
type: dep.type,
|
|
1657
|
+
originalLag,
|
|
1658
|
+
minLag,
|
|
1659
|
+
adjustable
|
|
1660
|
+
});
|
|
1661
|
+
};
|
|
1662
|
+
for (const { task } of subtreeTasks) {
|
|
1663
|
+
if (!task.dependencies) continue;
|
|
1664
|
+
task.dependencies.forEach((dep, depIndex) => {
|
|
1665
|
+
const predTask = taskById.get(dep.taskId);
|
|
1666
|
+
if (!predTask) return;
|
|
1667
|
+
const predInside = subtreeIds.has(predTask.id);
|
|
1668
|
+
const isOwnerLeaf = leafById.has(task.id);
|
|
1669
|
+
if (!predInside) {
|
|
1670
|
+
if (isOwnerLeaf) {
|
|
1671
|
+
const bounds = boundsByLeaf.get(task.id) ?? [];
|
|
1672
|
+
bounds.push({ leafId: task.id, type: dep.type, lag: dependencyLag(dep), predTask });
|
|
1673
|
+
boundsByLeaf.set(task.id, bounds);
|
|
1674
|
+
} else if (task.id === parentTask.id) {
|
|
1675
|
+
if (dep.type === "FS" || dep.type === "SS") {
|
|
1676
|
+
const bounds = boundsByLeaf.get(PARENT_BOUND_KEY) ?? [];
|
|
1677
|
+
bounds.push({ leafId: PARENT_BOUND_KEY, type: dep.type, lag: dependencyLag(dep), predTask });
|
|
1678
|
+
boundsByLeaf.set(PARENT_BOUND_KEY, bounds);
|
|
1679
|
+
}
|
|
1680
|
+
} else {
|
|
1681
|
+
const anchors = anchorLeafByParent.get(task.id);
|
|
1682
|
+
if (anchors) {
|
|
1683
|
+
const targetLeafId = dep.type === "FS" || dep.type === "SS" ? anchors.earliestStartId : anchors.latestEndId;
|
|
1684
|
+
const bounds = boundsByLeaf.get(targetLeafId) ?? [];
|
|
1685
|
+
bounds.push({ leafId: targetLeafId, type: dep.type, lag: dependencyLag(dep), predTask });
|
|
1686
|
+
boundsByLeaf.set(targetLeafId, bounds);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
let predLeafId;
|
|
1692
|
+
if (leafById.has(predTask.id)) {
|
|
1693
|
+
predLeafId = predTask.id;
|
|
1694
|
+
} else {
|
|
1695
|
+
const anchors = anchorLeafByParent.get(predTask.id);
|
|
1696
|
+
if (anchors) {
|
|
1697
|
+
predLeafId = dep.type === "FS" || dep.type === "FF" ? anchors.latestEndId : anchors.earliestStartId;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
let succLeafId;
|
|
1701
|
+
if (isOwnerLeaf) {
|
|
1702
|
+
succLeafId = task.id;
|
|
1703
|
+
} else {
|
|
1704
|
+
const anchors = anchorLeafByParent.get(task.id);
|
|
1705
|
+
if (anchors) {
|
|
1706
|
+
succLeafId = dep.type === "FS" || dep.type === "SS" ? anchors.earliestStartId : anchors.latestEndId;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (predLeafId && succLeafId && predLeafId !== succLeafId) {
|
|
1710
|
+
pushEdge(task, dep, depIndex, predLeafId, succLeafId);
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
if (lagErrors.length > 0) {
|
|
1715
|
+
return { ok: false, code: "INVALID_MINIMUM", details: lagErrors.slice(0, 5) };
|
|
1716
|
+
}
|
|
1717
|
+
for (const task of snapshot) {
|
|
1718
|
+
if (subtreeIds.has(task.id) || task.id === parentTask.id) continue;
|
|
1719
|
+
if (!task.dependencies) continue;
|
|
1720
|
+
for (const dep of task.dependencies) {
|
|
1721
|
+
if (!subtreeIds.has(dep.taskId) && dep.taskId !== parentTask.id) continue;
|
|
1722
|
+
outgoingRefs.push({ fromId: dep.taskId, succTask: task, type: dep.type, lag: dependencyLag(dep) });
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const inEdges = /* @__PURE__ */ new Map();
|
|
1726
|
+
const outEdges = /* @__PURE__ */ new Map();
|
|
1727
|
+
for (const edge of edges) {
|
|
1728
|
+
const inList = inEdges.get(edge.succLeafId) ?? [];
|
|
1729
|
+
inList.push(edge);
|
|
1730
|
+
inEdges.set(edge.succLeafId, inList);
|
|
1731
|
+
const outList = outEdges.get(edge.predLeafId) ?? [];
|
|
1732
|
+
outList.push(edge);
|
|
1733
|
+
outEdges.set(edge.predLeafId, outList);
|
|
1734
|
+
}
|
|
1735
|
+
for (const leaf of leaves) {
|
|
1736
|
+
leaf.isRoot = (inEdges.get(leaf.id)?.length ?? 0) === 0;
|
|
1737
|
+
}
|
|
1738
|
+
const componentRoot = new Map(leaves.map((leaf) => [leaf.id, leaf.id]));
|
|
1739
|
+
const find = (id) => {
|
|
1740
|
+
let root = componentRoot.get(id) ?? id;
|
|
1741
|
+
if (root === id) return root;
|
|
1742
|
+
root = find(root);
|
|
1743
|
+
componentRoot.set(id, root);
|
|
1744
|
+
return root;
|
|
1745
|
+
};
|
|
1746
|
+
const union = (a, b) => {
|
|
1747
|
+
const ra = find(a);
|
|
1748
|
+
const rb = find(b);
|
|
1749
|
+
if (ra !== rb) componentRoot.set(ra, rb);
|
|
1750
|
+
};
|
|
1751
|
+
for (const edge of edges) union(edge.predLeafId, edge.succLeafId);
|
|
1752
|
+
const componentIdByRoot = /* @__PURE__ */ new Map();
|
|
1753
|
+
let componentCount = 0;
|
|
1754
|
+
for (const leaf of leaves) {
|
|
1755
|
+
const root = find(leaf.id);
|
|
1756
|
+
if (!componentIdByRoot.has(root)) {
|
|
1757
|
+
componentIdByRoot.set(root, componentCount);
|
|
1758
|
+
componentCount++;
|
|
1759
|
+
}
|
|
1760
|
+
leaf.componentId = componentIdByRoot.get(root);
|
|
1761
|
+
}
|
|
1762
|
+
const componentFloor = /* @__PURE__ */ new Map();
|
|
1763
|
+
const parentBounds = boundsByLeaf.get(PARENT_BOUND_KEY) ?? [];
|
|
1764
|
+
for (const bound of parentBounds) {
|
|
1765
|
+
const { predStart, predEnd } = normalizePredecessorDates(bound.predTask, parseDateOnly);
|
|
1766
|
+
const boundDate = calculateSuccessorDate(
|
|
1767
|
+
predStart,
|
|
1768
|
+
predEnd,
|
|
1769
|
+
bound.type,
|
|
1770
|
+
bound.lag,
|
|
1771
|
+
businessDays,
|
|
1772
|
+
weekendPredicate
|
|
1773
|
+
);
|
|
1774
|
+
const boundIdx = space.idxOf(boundDate);
|
|
1775
|
+
for (const leaf of leaves) {
|
|
1776
|
+
if (!leaf.isRoot || leaf.immutable) continue;
|
|
1777
|
+
componentFloor.set(leaf.componentId, Math.max(componentFloor.get(leaf.componentId) ?? 0, boundIdx));
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
boundsByLeaf.delete(PARENT_BOUND_KEY);
|
|
1781
|
+
let adjustableTotal = 0;
|
|
1782
|
+
for (const leaf of leaves) {
|
|
1783
|
+
if (leaf.immutable || leaf.isMilestone) continue;
|
|
1784
|
+
adjustableTotal += Math.max(0, leaf.duration - leaf.minDuration);
|
|
1785
|
+
}
|
|
1786
|
+
for (const edge of edges) {
|
|
1787
|
+
if (edge.adjustable && edge.originalLag > edge.minLag) {
|
|
1788
|
+
adjustableTotal += edge.originalLag - edge.minLag;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
for (const leaf of leaves) {
|
|
1792
|
+
if (leaf.isRoot && !leaf.immutable) adjustableTotal += Math.max(0, leaf.startIdx);
|
|
1793
|
+
}
|
|
1794
|
+
const maxCorrectionSteps = 2 * (adjustableTotal + originalSpan) + targetBudgetGuard(originalSpan) + 64;
|
|
1795
|
+
const context = {
|
|
1796
|
+
businessDays,
|
|
1797
|
+
weekendPredicate,
|
|
1798
|
+
mirrored,
|
|
1799
|
+
parentTask,
|
|
1800
|
+
snapshot,
|
|
1801
|
+
subtreeIds,
|
|
1802
|
+
childrenByParentId,
|
|
1803
|
+
leaves,
|
|
1804
|
+
leafById,
|
|
1805
|
+
nestedParents,
|
|
1806
|
+
edges,
|
|
1807
|
+
inEdges,
|
|
1808
|
+
outEdges,
|
|
1809
|
+
boundsByLeaf,
|
|
1810
|
+
outgoingRefs,
|
|
1811
|
+
componentFloor,
|
|
1812
|
+
originalSpan,
|
|
1813
|
+
maxCorrectionSteps,
|
|
1814
|
+
isImmutable
|
|
1815
|
+
};
|
|
1816
|
+
return { ok: true, context, space };
|
|
1817
|
+
}
|
|
1818
|
+
function targetBudgetGuard(originalSpan) {
|
|
1819
|
+
return Math.max(1, originalSpan) * 2;
|
|
1820
|
+
}
|
|
1821
|
+
function isDescendantOf(task, ancestorId, taskById) {
|
|
1822
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1823
|
+
let currentId = task.parentId;
|
|
1824
|
+
while (currentId) {
|
|
1825
|
+
if (currentId === ancestorId) return true;
|
|
1826
|
+
if (visited.has(currentId)) return false;
|
|
1827
|
+
visited.add(currentId);
|
|
1828
|
+
currentId = taskById.get(currentId)?.parentId;
|
|
1829
|
+
}
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
function topoOrderLeaves(ctx, componentLeaves) {
|
|
1833
|
+
const byId = new Map(componentLeaves.map((leaf) => [leaf.id, leaf]));
|
|
1834
|
+
const indegree = new Map(
|
|
1835
|
+
componentLeaves.map((leaf) => [leaf.id, 0])
|
|
1836
|
+
);
|
|
1837
|
+
for (const leaf of componentLeaves) {
|
|
1838
|
+
for (const edge of ctx.inEdges.get(leaf.id) ?? []) {
|
|
1839
|
+
if (byId.has(edge.predLeafId)) {
|
|
1840
|
+
indegree.set(leaf.id, (indegree.get(leaf.id) ?? 0) + 1);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
const ready = componentLeaves.filter((leaf) => (indegree.get(leaf.id) ?? 0) === 0).sort((left, right) => left.snapshotIndex - right.snapshotIndex);
|
|
1845
|
+
const ordered = [];
|
|
1846
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1847
|
+
while (ready.length > 0) {
|
|
1848
|
+
const current = ready.shift();
|
|
1849
|
+
ordered.push(current);
|
|
1850
|
+
emitted.add(current.id);
|
|
1851
|
+
for (const edge of ctx.outEdges.get(current.id) ?? []) {
|
|
1852
|
+
if (!byId.has(edge.succLeafId) || emitted.has(edge.succLeafId)) continue;
|
|
1853
|
+
const next = (indegree.get(edge.succLeafId) ?? 0) - 1;
|
|
1854
|
+
indegree.set(edge.succLeafId, next);
|
|
1855
|
+
if (next === 0) {
|
|
1856
|
+
ready.push(byId.get(edge.succLeafId));
|
|
1857
|
+
ready.sort((left, right) => left.snapshotIndex - right.snapshotIndex);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
for (const leaf of componentLeaves) {
|
|
1862
|
+
if (!emitted.has(leaf.id)) ordered.push(leaf);
|
|
1863
|
+
}
|
|
1864
|
+
return ordered;
|
|
1865
|
+
}
|
|
1866
|
+
function evaluateLayout(ctx, space, state) {
|
|
1867
|
+
const { businessDays, weekendPredicate } = ctx;
|
|
1868
|
+
const dates = /* @__PURE__ */ new Map();
|
|
1869
|
+
for (const leaf of ctx.leaves) {
|
|
1870
|
+
if (leaf.immutable) {
|
|
1871
|
+
dates.set(leaf.id, {
|
|
1872
|
+
start: parseDateOnly(leaf.task.startDate),
|
|
1873
|
+
end: parseDateOnly(leaf.task.endDate)
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const componentOrder = [];
|
|
1878
|
+
const componentLeaves = /* @__PURE__ */ new Map();
|
|
1879
|
+
for (const leaf of ctx.leaves) {
|
|
1880
|
+
const list = componentLeaves.get(leaf.componentId) ?? [];
|
|
1881
|
+
list.push(leaf);
|
|
1882
|
+
componentLeaves.set(leaf.componentId, list);
|
|
1883
|
+
if (list.length === 1) componentOrder.push(leaf.componentId);
|
|
1884
|
+
}
|
|
1885
|
+
for (const componentId of componentOrder) {
|
|
1886
|
+
const members = componentLeaves.get(componentId) ?? [];
|
|
1887
|
+
const order = topoOrderLeaves(ctx, members);
|
|
1888
|
+
for (const leaf of order) {
|
|
1889
|
+
if (dates.has(leaf.id)) continue;
|
|
1890
|
+
const duration = state.durations.get(leaf.id) ?? leaf.duration;
|
|
1891
|
+
let range = null;
|
|
1892
|
+
const incoming = ctx.inEdges.get(leaf.id) ?? [];
|
|
1893
|
+
if (incoming.length === 0) {
|
|
1894
|
+
const floor = ctx.componentFloor.get(leaf.componentId) ?? 0;
|
|
1895
|
+
const offset = Math.max(floor, state.offsets.get(leaf.id) ?? leaf.startIdx);
|
|
1896
|
+
range = buildTaskRangeFromStart(space.dateOf(offset), duration, businessDays, weekendPredicate);
|
|
1897
|
+
} else {
|
|
1898
|
+
for (const edge of incoming) {
|
|
1899
|
+
const predDates = dates.get(edge.predLeafId);
|
|
1900
|
+
if (!predDates) continue;
|
|
1901
|
+
const predLeaf = ctx.leafById.get(edge.predLeafId);
|
|
1902
|
+
const { predStart, predEnd } = normalizePredecessorDates(
|
|
1903
|
+
{
|
|
1904
|
+
startDate: predDates.start,
|
|
1905
|
+
endDate: predDates.end,
|
|
1906
|
+
type: predLeaf?.isMilestone ? "milestone" : "task"
|
|
1907
|
+
},
|
|
1908
|
+
parseDateOnly
|
|
1909
|
+
);
|
|
1910
|
+
const lag = state.lags.get(edge.key) ?? edge.originalLag;
|
|
1911
|
+
const constraint = calculateSuccessorDate(
|
|
1912
|
+
predStart,
|
|
1913
|
+
predEnd,
|
|
1914
|
+
edge.type,
|
|
1915
|
+
lag,
|
|
1916
|
+
businessDays,
|
|
1917
|
+
weekendPredicate,
|
|
1918
|
+
leaf.isMilestone ? "milestone" : void 0
|
|
1919
|
+
);
|
|
1920
|
+
const candidate = edge.type === "FS" || edge.type === "SS" ? buildTaskRangeFromStart(constraint, duration, businessDays, weekendPredicate) : buildTaskRangeFromEnd(constraint, duration, businessDays, weekendPredicate);
|
|
1921
|
+
if (!range || candidate.start.getTime() > range.start.getTime() || candidate.start.getTime() === range.start.getTime() && candidate.end.getTime() > range.end.getTime()) {
|
|
1922
|
+
range = candidate;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (!range) {
|
|
1927
|
+
range = buildTaskRangeFromStart(space.dateOf(leaf.startIdx), duration, businessDays, weekendPredicate);
|
|
1928
|
+
}
|
|
1929
|
+
for (const bound of ctx.boundsByLeaf.get(leaf.id) ?? []) {
|
|
1930
|
+
const { predStart, predEnd } = normalizePredecessorDates(bound.predTask, parseDateOnly);
|
|
1931
|
+
const boundDate = calculateSuccessorDate(
|
|
1932
|
+
predStart,
|
|
1933
|
+
predEnd,
|
|
1934
|
+
bound.type,
|
|
1935
|
+
bound.lag,
|
|
1936
|
+
businessDays,
|
|
1937
|
+
weekendPredicate,
|
|
1938
|
+
leaf.isMilestone ? "milestone" : void 0
|
|
1939
|
+
);
|
|
1940
|
+
if ((bound.type === "FS" || bound.type === "SS") && range.start.getTime() < boundDate.getTime()) {
|
|
1941
|
+
range = buildTaskRangeFromStart(boundDate, duration, businessDays, weekendPredicate);
|
|
1942
|
+
} else if ((bound.type === "FF" || bound.type === "SF") && range.end.getTime() < boundDate.getTime()) {
|
|
1943
|
+
range = buildTaskRangeFromEnd(boundDate, duration, businessDays, weekendPredicate);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
for (const edge of ctx.outEdges.get(leaf.id) ?? []) {
|
|
1947
|
+
const succLeaf = ctx.leafById.get(edge.succLeafId);
|
|
1948
|
+
if (!succLeaf || !succLeaf.immutable) continue;
|
|
1949
|
+
const succDates = dates.get(succLeaf.id);
|
|
1950
|
+
if (!succDates) continue;
|
|
1951
|
+
const lag = state.lags.get(edge.key) ?? edge.originalLag;
|
|
1952
|
+
range = applyReverseBound(
|
|
1953
|
+
range,
|
|
1954
|
+
edge.type,
|
|
1955
|
+
lag,
|
|
1956
|
+
succDates,
|
|
1957
|
+
succLeaf.isMilestone,
|
|
1958
|
+
duration,
|
|
1959
|
+
businessDays,
|
|
1960
|
+
weekendPredicate
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
range = clampToAnchor(range, space, duration, businessDays, weekendPredicate);
|
|
1964
|
+
dates.set(leaf.id, range);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
let lo = Number.POSITIVE_INFINITY;
|
|
1968
|
+
let hi = Number.NEGATIVE_INFINITY;
|
|
1969
|
+
for (const leaf of ctx.leaves) {
|
|
1970
|
+
const range = dates.get(leaf.id);
|
|
1971
|
+
if (!range) continue;
|
|
1972
|
+
const startIdx = space.idxOf(range.start);
|
|
1973
|
+
const endIdx = space.idxOf(range.end);
|
|
1974
|
+
lo = Math.min(lo, startIdx, endIdx);
|
|
1975
|
+
hi = Math.max(hi, startIdx, endIdx);
|
|
1976
|
+
}
|
|
1977
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
|
|
1978
|
+
return { dates, lo: 0, hi: 0, span: 1 };
|
|
1979
|
+
}
|
|
1980
|
+
return { dates, lo, hi, span: hi - lo + 1 };
|
|
1981
|
+
}
|
|
1982
|
+
function applyReverseBound(range, type, lag, succDates, succIsMilestone, duration, businessDays, weekendPredicate) {
|
|
1983
|
+
const shiftBack = (date, days) => businessDays && weekendPredicate ? shiftBusinessDayOffset(date, -days, weekendPredicate) : new Date(date.getTime() - days * DAY_MS);
|
|
1984
|
+
switch (type) {
|
|
1985
|
+
case "FS": {
|
|
1986
|
+
const allowedEnd = shiftBack(succDates.start, lag + (succIsMilestone ? 0 : 1));
|
|
1987
|
+
return range.end.getTime() > allowedEnd.getTime() ? buildTaskRangeFromEnd(allowedEnd, duration, businessDays, weekendPredicate) : range;
|
|
1988
|
+
}
|
|
1989
|
+
case "SS": {
|
|
1990
|
+
const allowedStart = shiftBack(succDates.start, lag);
|
|
1991
|
+
return range.start.getTime() > allowedStart.getTime() ? buildTaskRangeFromStart(allowedStart, duration, businessDays, weekendPredicate) : range;
|
|
1992
|
+
}
|
|
1993
|
+
case "FF": {
|
|
1994
|
+
const allowedEnd = shiftBack(succDates.end, lag);
|
|
1995
|
+
return range.end.getTime() > allowedEnd.getTime() ? buildTaskRangeFromEnd(allowedEnd, duration, businessDays, weekendPredicate) : range;
|
|
1996
|
+
}
|
|
1997
|
+
case "SF": {
|
|
1998
|
+
const allowedStart = shiftBack(succDates.end, lag - 1);
|
|
1999
|
+
return range.start.getTime() > allowedStart.getTime() ? buildTaskRangeFromStart(allowedStart, duration, businessDays, weekendPredicate) : range;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
function clampToAnchor(range, space, duration, businessDays, weekendPredicate) {
|
|
2004
|
+
if (!space.mirrored) {
|
|
2005
|
+
if (space.idxOf(range.start) < 0) {
|
|
2006
|
+
return buildTaskRangeFromStart(space.anchorDate, duration, businessDays, weekendPredicate);
|
|
2007
|
+
}
|
|
2008
|
+
} else if (space.idxOf(range.end) < 0) {
|
|
2009
|
+
return buildTaskRangeFromEnd(space.anchorDate, duration, businessDays, weekendPredicate);
|
|
2010
|
+
}
|
|
2011
|
+
return range;
|
|
2012
|
+
}
|
|
2013
|
+
function allocateDurations(ctx, target) {
|
|
2014
|
+
const durations = /* @__PURE__ */ new Map();
|
|
2015
|
+
const ratio = target / Math.max(1, ctx.originalSpan);
|
|
2016
|
+
for (const leaf of ctx.leaves) {
|
|
2017
|
+
if (leaf.immutable || leaf.isMilestone) {
|
|
2018
|
+
durations.set(leaf.id, leaf.duration);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const ideals = ctx.leaves.filter((leaf) => !leaf.immutable && !leaf.isMilestone).map((leaf) => ({ leaf, ideal: Math.max(leaf.minDuration, leaf.duration * ratio) }));
|
|
2022
|
+
const minTotal = ideals.reduce((sum, { leaf }) => sum + leaf.minDuration, 0);
|
|
2023
|
+
const idealTotal = ideals.reduce((sum, { ideal }) => sum + ideal, 0);
|
|
2024
|
+
const total = Math.max(minTotal, Math.round(idealTotal));
|
|
2025
|
+
const floors = /* @__PURE__ */ new Map();
|
|
2026
|
+
let floorTotal = 0;
|
|
2027
|
+
for (const { leaf, ideal } of ideals) {
|
|
2028
|
+
const floor = Math.max(leaf.minDuration, Math.floor(ideal));
|
|
2029
|
+
floors.set(leaf.id, floor);
|
|
2030
|
+
floorTotal += floor;
|
|
2031
|
+
}
|
|
2032
|
+
let remaining = Math.max(0, total - floorTotal);
|
|
2033
|
+
const byFraction = [...ideals].sort((left, right) => {
|
|
2034
|
+
const fracLeft = left.ideal - (floors.get(left.leaf.id) ?? 0);
|
|
2035
|
+
const fracRight = right.ideal - (floors.get(right.leaf.id) ?? 0);
|
|
2036
|
+
if (fracLeft !== fracRight) return fracRight - fracLeft;
|
|
2037
|
+
const timeLeft = isoOfTaskDate(left.leaf.task.startDate);
|
|
2038
|
+
const timeRight = isoOfTaskDate(right.leaf.task.startDate);
|
|
2039
|
+
if (timeLeft !== timeRight) return timeLeft < timeRight ? -1 : 1;
|
|
2040
|
+
return left.leaf.id < right.leaf.id ? -1 : 1;
|
|
2041
|
+
});
|
|
2042
|
+
for (const { leaf } of byFraction) {
|
|
2043
|
+
if (remaining <= 0) break;
|
|
2044
|
+
floors.set(leaf.id, (floors.get(leaf.id) ?? 0) + 1);
|
|
2045
|
+
remaining -= 1;
|
|
2046
|
+
}
|
|
2047
|
+
for (const { leaf } of ideals) {
|
|
2048
|
+
durations.set(leaf.id, floors.get(leaf.id) ?? leaf.duration);
|
|
2049
|
+
}
|
|
2050
|
+
return durations;
|
|
2051
|
+
}
|
|
2052
|
+
function solveScale(ctx, space, target) {
|
|
2053
|
+
const state = {
|
|
2054
|
+
durations: allocateDurations(ctx, target),
|
|
2055
|
+
lags: new Map(ctx.edges.map((edge) => [edge.key, edge.originalLag])),
|
|
2056
|
+
offsets: new Map(
|
|
2057
|
+
ctx.leaves.filter((leaf) => leaf.isRoot && !leaf.immutable).map((leaf) => {
|
|
2058
|
+
const floor = ctx.componentFloor.get(leaf.componentId) ?? 0;
|
|
2059
|
+
const scaled = Math.round(leaf.startIdx * target / Math.max(1, ctx.originalSpan));
|
|
2060
|
+
return [leaf.id, Math.max(floor, scaled)];
|
|
2061
|
+
})
|
|
2062
|
+
)
|
|
2063
|
+
};
|
|
2064
|
+
let layout = evaluateLayout(ctx, space, state);
|
|
2065
|
+
let exhausted = false;
|
|
2066
|
+
let steps = 0;
|
|
2067
|
+
while (layout.span !== target && steps < ctx.maxCorrectionSteps) {
|
|
2068
|
+
steps++;
|
|
2069
|
+
if (layout.span > target) {
|
|
2070
|
+
if (tryFirstImprovementReduce(ctx, space, state, layout)) {
|
|
2071
|
+
layout = evaluateLayout(ctx, space, state);
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
if (exhausted) break;
|
|
2075
|
+
exhausted = true;
|
|
2076
|
+
exhaustFlexibleGaps(ctx, state);
|
|
2077
|
+
layout = evaluateLayout(ctx, space, state);
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
if (tryGrow(ctx, space, state, layout, target)) {
|
|
2081
|
+
layout = evaluateLayout(ctx, space, state);
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
layout = alignAnchorBoundary(ctx, space, state, layout);
|
|
2087
|
+
return {
|
|
2088
|
+
state,
|
|
2089
|
+
layout,
|
|
2090
|
+
appliedSpan: layout.span,
|
|
2091
|
+
clamped: layout.span > target
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
function farBoundaryKey(leaf, dates, space) {
|
|
2095
|
+
const range = dates.get(leaf.id);
|
|
2096
|
+
if (!range) return Math.max(leaf.startIdx, leaf.endIdx);
|
|
2097
|
+
return Math.max(space.idxOf(range.start), space.idxOf(range.end));
|
|
2098
|
+
}
|
|
2099
|
+
function tryFirstImprovementReduce(ctx, space, state, layout) {
|
|
2100
|
+
const durationCandidates = ctx.leaves.filter(
|
|
2101
|
+
(leaf) => !leaf.immutable && !leaf.isMilestone && (state.durations.get(leaf.id) ?? leaf.duration) > leaf.minDuration
|
|
2102
|
+
).sort(
|
|
2103
|
+
(left, right) => farBoundaryKey(right, layout.dates, space) - farBoundaryKey(left, layout.dates, space) || left.snapshotIndex - right.snapshotIndex
|
|
2104
|
+
);
|
|
2105
|
+
for (const leaf of durationCandidates) {
|
|
2106
|
+
const current = state.durations.get(leaf.id) ?? leaf.duration;
|
|
2107
|
+
state.durations.set(leaf.id, current - 1);
|
|
2108
|
+
const next = evaluateLayout(ctx, space, state);
|
|
2109
|
+
if (next.span < layout.span) return true;
|
|
2110
|
+
state.durations.set(leaf.id, current);
|
|
2111
|
+
}
|
|
2112
|
+
const lagCandidates = ctx.edges.filter((edge) => edge.adjustable && (state.lags.get(edge.key) ?? edge.originalLag) > edge.minLag).sort((left, right) => {
|
|
2113
|
+
const succLeft = ctx.leafById.get(left.succLeafId)?.snapshotIndex ?? 0;
|
|
2114
|
+
const succRight = ctx.leafById.get(right.succLeafId)?.snapshotIndex ?? 0;
|
|
2115
|
+
if (succLeft !== succRight) return succLeft - succRight;
|
|
2116
|
+
return left.key < right.key ? -1 : 1;
|
|
2117
|
+
});
|
|
2118
|
+
for (const edge of lagCandidates) {
|
|
2119
|
+
const current = state.lags.get(edge.key) ?? edge.originalLag;
|
|
2120
|
+
state.lags.set(edge.key, current - 1);
|
|
2121
|
+
const next = evaluateLayout(ctx, space, state);
|
|
2122
|
+
if (next.span < layout.span) return true;
|
|
2123
|
+
state.lags.set(edge.key, current);
|
|
2124
|
+
}
|
|
2125
|
+
const offsetCandidates = ctx.leaves.filter((leaf) => leaf.isRoot && !leaf.immutable).map((leaf) => ({ leaf, offset: state.offsets.get(leaf.id) ?? leaf.startIdx })).filter(({ leaf, offset }) => offset > (ctx.componentFloor.get(leaf.componentId) ?? 0)).sort(
|
|
2126
|
+
(left, right) => right.offset - left.offset || left.leaf.snapshotIndex - right.leaf.snapshotIndex
|
|
2127
|
+
);
|
|
2128
|
+
for (const { leaf, offset } of offsetCandidates) {
|
|
2129
|
+
state.offsets.set(leaf.id, offset - 1);
|
|
2130
|
+
const next = evaluateLayout(ctx, space, state);
|
|
2131
|
+
if (next.span < layout.span) return true;
|
|
2132
|
+
state.offsets.set(leaf.id, offset);
|
|
2133
|
+
}
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
function exhaustFlexibleGaps(ctx, state) {
|
|
2137
|
+
for (const leaf of ctx.leaves) {
|
|
2138
|
+
if (leaf.immutable || leaf.isMilestone) continue;
|
|
2139
|
+
state.durations.set(leaf.id, leaf.minDuration);
|
|
2140
|
+
}
|
|
2141
|
+
for (const edge of ctx.edges) {
|
|
2142
|
+
if (edge.adjustable) {
|
|
2143
|
+
state.lags.set(edge.key, edge.minLag);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
for (const leaf of ctx.leaves) {
|
|
2147
|
+
if (!leaf.isRoot || leaf.immutable) continue;
|
|
2148
|
+
state.offsets.set(leaf.id, ctx.componentFloor.get(leaf.componentId) ?? 0);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function tryGrow(ctx, space, state, layout, target) {
|
|
2152
|
+
const growthCandidates = ctx.leaves.filter((leaf) => !leaf.immutable && !leaf.isMilestone).sort(
|
|
2153
|
+
(left, right) => farBoundaryKey(right, layout.dates, space) - farBoundaryKey(left, layout.dates, space) || left.snapshotIndex - right.snapshotIndex
|
|
2154
|
+
);
|
|
2155
|
+
for (const leaf of growthCandidates) {
|
|
2156
|
+
const current = state.durations.get(leaf.id) ?? leaf.duration;
|
|
2157
|
+
state.durations.set(leaf.id, current + 1);
|
|
2158
|
+
const next = evaluateLayout(ctx, space, state);
|
|
2159
|
+
if (next.span > layout.span && next.span <= target) return true;
|
|
2160
|
+
state.durations.set(leaf.id, current);
|
|
2161
|
+
}
|
|
2162
|
+
const offsetCandidates = ctx.leaves.filter((leaf) => leaf.isRoot && !leaf.immutable).map((leaf) => ({ leaf, offset: state.offsets.get(leaf.id) ?? leaf.startIdx })).sort(
|
|
2163
|
+
(left, right) => right.offset - left.offset || left.leaf.snapshotIndex - right.leaf.snapshotIndex
|
|
2164
|
+
);
|
|
2165
|
+
for (const { leaf, offset } of offsetCandidates) {
|
|
2166
|
+
state.offsets.set(leaf.id, offset + 1);
|
|
2167
|
+
const next = evaluateLayout(ctx, space, state);
|
|
2168
|
+
if (next.span > layout.span && next.span <= target) return true;
|
|
2169
|
+
state.offsets.set(leaf.id, offset);
|
|
2170
|
+
}
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
function alignAnchorBoundary(ctx, space, state, layout) {
|
|
2174
|
+
if (layout.lo === 0) return layout;
|
|
2175
|
+
if (ctx.leaves.some((leaf) => leaf.immutable)) return layout;
|
|
2176
|
+
if (ctx.boundsByLeaf.size > 0) return layout;
|
|
2177
|
+
if (ctx.componentFloor.size > 0) return layout;
|
|
2178
|
+
const roots = ctx.leaves.filter((leaf) => leaf.isRoot && !leaf.immutable);
|
|
2179
|
+
if (roots.length === 0) return layout;
|
|
2180
|
+
const minOffset = Math.min(
|
|
2181
|
+
...roots.map((leaf) => state.offsets.get(leaf.id) ?? leaf.startIdx)
|
|
2182
|
+
);
|
|
2183
|
+
if (minOffset <= 0) return layout;
|
|
2184
|
+
const shift = Math.min(layout.lo, minOffset);
|
|
2185
|
+
if (shift <= 0) return layout;
|
|
2186
|
+
const backup = new Map(state.offsets);
|
|
2187
|
+
for (const leaf of roots) {
|
|
2188
|
+
state.offsets.set(leaf.id, (state.offsets.get(leaf.id) ?? leaf.startIdx) - shift);
|
|
2189
|
+
}
|
|
2190
|
+
const next = evaluateLayout(ctx, space, state);
|
|
2191
|
+
if (next.span === layout.span && next.lo < layout.lo) {
|
|
2192
|
+
return next;
|
|
2193
|
+
}
|
|
2194
|
+
state.offsets.clear();
|
|
2195
|
+
for (const [key, value] of backup) state.offsets.set(key, value);
|
|
2196
|
+
return layout;
|
|
2197
|
+
}
|
|
2198
|
+
function buildLeafDependencies(leaf, range, finalDates, taskById, state, ctx, space) {
|
|
2199
|
+
const deps = leaf.task.dependencies;
|
|
2200
|
+
if (!deps || deps.length === 0) return null;
|
|
2201
|
+
let changed = false;
|
|
2202
|
+
const nextDeps = deps.map((dep, depIndex) => {
|
|
2203
|
+
const key = `${leaf.id}#${depIndex}`;
|
|
2204
|
+
const solvedLag = state.lags.get(key);
|
|
2205
|
+
if (solvedLag !== void 0 && solvedLag !== dependencyLag(dep)) {
|
|
2206
|
+
changed = true;
|
|
2207
|
+
return { ...dep, lag: solvedLag };
|
|
2208
|
+
}
|
|
2209
|
+
const predTask = taskById.get(dep.taskId);
|
|
2210
|
+
if (!predTask) return { ...dep, lag: dependencyLag(dep) };
|
|
2211
|
+
if (ctx.subtreeIds.has(predTask.id)) {
|
|
2212
|
+
return { ...dep, lag: dependencyLag(dep) };
|
|
2213
|
+
}
|
|
2214
|
+
const predRange = finalDates.get(predTask.id);
|
|
2215
|
+
const { predStart, predEnd } = normalizePredecessorDates(
|
|
2216
|
+
{
|
|
2217
|
+
startDate: predRange ? predRange.start : parseDateOnly(predTask.startDate),
|
|
2218
|
+
endDate: predRange ? predRange.end : parseDateOnly(predTask.endDate),
|
|
2219
|
+
type: predTask.type
|
|
2220
|
+
},
|
|
2221
|
+
parseDateOnly
|
|
2222
|
+
);
|
|
2223
|
+
const lag = computeLagFromDates(
|
|
2224
|
+
dep.type,
|
|
2225
|
+
predStart,
|
|
2226
|
+
predEnd,
|
|
2227
|
+
range.start,
|
|
2228
|
+
range.end,
|
|
2229
|
+
ctx.businessDays,
|
|
2230
|
+
ctx.weekendPredicate,
|
|
2231
|
+
leaf.isMilestone ? "milestone" : void 0
|
|
2232
|
+
);
|
|
2233
|
+
if (lag !== dependencyLag(dep)) changed = true;
|
|
2234
|
+
return { ...dep, lag };
|
|
2235
|
+
});
|
|
2236
|
+
void space;
|
|
2237
|
+
return changed ? nextDeps : null;
|
|
2238
|
+
}
|
|
2239
|
+
function buildFinalTasks(ctx, state, layout, space) {
|
|
2240
|
+
const taskById = new Map(ctx.snapshot.map((task) => [task.id, task]));
|
|
2241
|
+
const finalTasks = /* @__PURE__ */ new Map();
|
|
2242
|
+
for (const leaf of ctx.leaves) {
|
|
2243
|
+
if (leaf.immutable) continue;
|
|
2244
|
+
const range = layout.dates.get(leaf.id);
|
|
2245
|
+
if (!range) continue;
|
|
2246
|
+
const newStart = toIsoDate2(range.start);
|
|
2247
|
+
const newEnd = toIsoDate2(range.end);
|
|
2248
|
+
const startChanged = newStart !== isoOfTaskDate(leaf.task.startDate);
|
|
2249
|
+
const endChanged = newEnd !== isoOfTaskDate(leaf.task.endDate);
|
|
2250
|
+
const nextDeps = buildLeafDependencies(leaf, range, layout.dates, taskById, state, ctx, space);
|
|
2251
|
+
if (!startChanged && !endChanged && !nextDeps) continue;
|
|
2252
|
+
finalTasks.set(leaf.id, {
|
|
2253
|
+
...leaf.task,
|
|
2254
|
+
startDate: newStart,
|
|
2255
|
+
endDate: newEnd,
|
|
2256
|
+
...nextDeps ? { dependencies: nextDeps } : {}
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
for (const { task } of ctx.nestedParents) {
|
|
2260
|
+
const children = ctx.childrenByParentId.get(task.id) ?? [];
|
|
2261
|
+
let minStart = null;
|
|
2262
|
+
let maxEnd = null;
|
|
2263
|
+
for (const child of children) {
|
|
2264
|
+
const childFinal = finalTasks.get(child.id);
|
|
2265
|
+
const start = parseDateOnly(childFinal ? childFinal.startDate : child.startDate);
|
|
2266
|
+
const end = parseDateOnly(childFinal ? childFinal.endDate : child.endDate);
|
|
2267
|
+
if (!minStart || start.getTime() < minStart.getTime()) minStart = start;
|
|
2268
|
+
if (!maxEnd || end.getTime() > maxEnd.getTime()) maxEnd = end;
|
|
2269
|
+
}
|
|
2270
|
+
if (!minStart || !maxEnd) continue;
|
|
2271
|
+
const newStart = toIsoDate2(minStart);
|
|
2272
|
+
const newEnd = toIsoDate2(maxEnd);
|
|
2273
|
+
if (newStart === isoOfTaskDate(task.startDate) && newEnd === isoOfTaskDate(task.endDate)) {
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
finalTasks.set(task.id, { ...task, startDate: newStart, endDate: newEnd });
|
|
2277
|
+
}
|
|
2278
|
+
const parentChildren = ctx.childrenByParentId.get(ctx.parentTask.id) ?? [];
|
|
2279
|
+
let parentStart = null;
|
|
2280
|
+
let parentEnd = null;
|
|
2281
|
+
for (const child of parentChildren) {
|
|
2282
|
+
const childFinal = finalTasks.get(child.id);
|
|
2283
|
+
const start = parseDateOnly(childFinal ? childFinal.startDate : child.startDate);
|
|
2284
|
+
const end = parseDateOnly(childFinal ? childFinal.endDate : child.endDate);
|
|
2285
|
+
if (!parentStart || start.getTime() < parentStart.getTime()) parentStart = start;
|
|
2286
|
+
if (!parentEnd || end.getTime() > parentEnd.getTime()) parentEnd = end;
|
|
2287
|
+
}
|
|
2288
|
+
if (parentStart && parentEnd) {
|
|
2289
|
+
const newStart = toIsoDate2(parentStart);
|
|
2290
|
+
const newEnd = toIsoDate2(parentEnd);
|
|
2291
|
+
if (newStart !== isoOfTaskDate(ctx.parentTask.startDate) || newEnd !== isoOfTaskDate(ctx.parentTask.endDate)) {
|
|
2292
|
+
finalTasks.set(ctx.parentTask.id, {
|
|
2293
|
+
...ctx.parentTask,
|
|
2294
|
+
startDate: newStart,
|
|
2295
|
+
endDate: newEnd
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
return finalTasks;
|
|
2300
|
+
}
|
|
2301
|
+
function findOutgoingViolations(ctx, finalTasks) {
|
|
2302
|
+
const violations = [];
|
|
2303
|
+
const originalById = new Map(ctx.snapshot.map((task) => [task.id, task]));
|
|
2304
|
+
for (const ref of ctx.outgoingRefs) {
|
|
2305
|
+
const fromTask = finalTasks.get(ref.fromId) ?? originalById.get(ref.fromId);
|
|
2306
|
+
if (!fromTask) continue;
|
|
2307
|
+
const { predStart, predEnd } = normalizePredecessorDates(fromTask, parseDateOnly);
|
|
2308
|
+
const required = calculateSuccessorDate(
|
|
2309
|
+
predStart,
|
|
2310
|
+
predEnd,
|
|
2311
|
+
ref.type,
|
|
2312
|
+
ref.lag,
|
|
2313
|
+
ctx.businessDays,
|
|
2314
|
+
ctx.weekendPredicate,
|
|
2315
|
+
ref.succTask.type === "milestone" ? "milestone" : void 0
|
|
2316
|
+
);
|
|
2317
|
+
const succStart = parseDateOnly(ref.succTask.startDate);
|
|
2318
|
+
const succEnd = ref.succTask.type === "milestone" ? succStart : parseDateOnly(ref.succTask.endDate);
|
|
2319
|
+
if (ref.type === "FS" || ref.type === "SS") {
|
|
2320
|
+
if (succStart.getTime() < required.getTime()) {
|
|
2321
|
+
violations.push(`${ref.type} ${ref.fromId} -> ${ref.succTask.id}`);
|
|
2322
|
+
}
|
|
2323
|
+
} else if (succEnd.getTime() < required.getTime()) {
|
|
2324
|
+
violations.push(`${ref.type} ${ref.fromId} -> ${ref.succTask.id}`);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return violations;
|
|
2328
|
+
}
|
|
2329
|
+
function cascadeExternalSuccessors(ctx, finalTasks) {
|
|
2330
|
+
const immutableExternal = /* @__PURE__ */ new Set();
|
|
2331
|
+
const seeds = [];
|
|
2332
|
+
for (const ref of ctx.outgoingRefs) {
|
|
2333
|
+
if (ctx.isImmutable(ref.succTask) || ref.succTask.locked === true) {
|
|
2334
|
+
immutableExternal.add(ref.succTask.id);
|
|
2335
|
+
}
|
|
2336
|
+
if (finalTasks.has(ref.fromId) && !seeds.includes(ref.fromId)) {
|
|
2337
|
+
seeds.push(ref.fromId);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (immutableExternal.size > 0) {
|
|
2341
|
+
return {
|
|
2342
|
+
ok: false,
|
|
2343
|
+
details: [`immutable external successor(s): ${[...immutableExternal].slice(0, 5).join(", ")}`]
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
if (seeds.length === 0) return { ok: true, changedExternal: [] };
|
|
2347
|
+
const working = ctx.snapshot.map((task) => {
|
|
2348
|
+
const final = finalTasks.get(task.id);
|
|
2349
|
+
return final ?? { ...task };
|
|
2350
|
+
});
|
|
2351
|
+
const externalChanged = /* @__PURE__ */ new Map();
|
|
2352
|
+
for (const seedId of seeds) {
|
|
2353
|
+
const seedTask = working.find((task) => task.id === seedId);
|
|
2354
|
+
if (!seedTask) continue;
|
|
2355
|
+
const cascade = universalCascade(
|
|
2356
|
+
seedTask,
|
|
2357
|
+
parseDateOnly(seedTask.startDate),
|
|
2358
|
+
parseDateOnly(seedTask.endDate),
|
|
2359
|
+
working,
|
|
2360
|
+
ctx.businessDays,
|
|
2361
|
+
ctx.weekendPredicate
|
|
2362
|
+
);
|
|
2363
|
+
for (const updated of cascade) {
|
|
2364
|
+
if (ctx.subtreeIds.has(updated.id) || updated.id === ctx.parentTask.id) continue;
|
|
2365
|
+
const beforeIndex = working.findIndex((task) => task.id === updated.id);
|
|
2366
|
+
if (beforeIndex < 0) continue;
|
|
2367
|
+
const before = working[beforeIndex];
|
|
2368
|
+
if (JSON.stringify(before) !== JSON.stringify(updated)) {
|
|
2369
|
+
externalChanged.set(updated.id, updated);
|
|
2370
|
+
}
|
|
2371
|
+
working.splice(beforeIndex, 1, updated);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
const changedSet = new Set(externalChanged.keys());
|
|
2375
|
+
const indegree = new Map([...changedSet].map((id) => [id, 0]));
|
|
2376
|
+
for (const task of externalChanged.values()) {
|
|
2377
|
+
for (const dep of task.dependencies ?? []) {
|
|
2378
|
+
if (changedSet.has(dep.taskId)) {
|
|
2379
|
+
indegree.set(task.id, (indegree.get(task.id) ?? 0) + 1);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const snapshotIndexById = new Map(ctx.snapshot.map((task, index) => [task.id, index]));
|
|
2384
|
+
const ready = [...changedSet].filter((id) => (indegree.get(id) ?? 0) === 0).sort((left, right) => (snapshotIndexById.get(left) ?? 0) - (snapshotIndexById.get(right) ?? 0));
|
|
2385
|
+
const ordered = [];
|
|
2386
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2387
|
+
while (ready.length > 0) {
|
|
2388
|
+
const currentId = ready.shift();
|
|
2389
|
+
const current = externalChanged.get(currentId);
|
|
2390
|
+
ordered.push(current);
|
|
2391
|
+
emitted.add(currentId);
|
|
2392
|
+
for (const [otherId, other] of externalChanged) {
|
|
2393
|
+
if (emitted.has(otherId)) continue;
|
|
2394
|
+
if (!(other.dependencies ?? []).some((dep) => dep.taskId === currentId)) continue;
|
|
2395
|
+
const next = (indegree.get(otherId) ?? 0) - 1;
|
|
2396
|
+
indegree.set(otherId, next);
|
|
2397
|
+
if (next === 0) {
|
|
2398
|
+
ready.push(otherId);
|
|
2399
|
+
ready.sort((left, right) => (snapshotIndexById.get(left) ?? 0) - (snapshotIndexById.get(right) ?? 0));
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
for (const id of changedSet) {
|
|
2404
|
+
if (!emitted.has(id)) ordered.push(externalChanged.get(id));
|
|
2405
|
+
}
|
|
2406
|
+
return { ok: true, changedExternal: ordered };
|
|
2407
|
+
}
|
|
2408
|
+
function validateFinalGraph(ctx, finalTasks, external) {
|
|
2409
|
+
if (finalTasks.size === 0 && external.length === 0) return [];
|
|
2410
|
+
const updates = /* @__PURE__ */ new Map();
|
|
2411
|
+
for (const task of finalTasks.values()) updates.set(task.id, task);
|
|
2412
|
+
for (const task of external) updates.set(task.id, task);
|
|
2413
|
+
const finalSnapshot = ctx.snapshot.map((task) => updates.get(task.id) ?? task);
|
|
2414
|
+
const validation = validateDependencies(finalSnapshot);
|
|
2415
|
+
return validation.isValid ? [] : validation.errors.slice(0, 5).map((error) => `${error.type}: ${error.taskId}`);
|
|
2416
|
+
}
|
|
2417
|
+
function scaleTaskSubtreeDuration(parentId, targetDuration, snapshot, options) {
|
|
2418
|
+
const resolvedOptions = options ?? {};
|
|
2419
|
+
const businessDays = resolvedOptions.businessDays ?? false;
|
|
2420
|
+
const validation = validateScaleInput(
|
|
2421
|
+
parentId,
|
|
2422
|
+
targetDuration,
|
|
2423
|
+
snapshot,
|
|
2424
|
+
businessDays,
|
|
2425
|
+
resolvedOptions.weekendPredicate
|
|
2426
|
+
);
|
|
2427
|
+
if (!validation.ok) {
|
|
2428
|
+
return scaleFailure(validation.code, validation.details);
|
|
2429
|
+
}
|
|
2430
|
+
const built = buildScaleContext(validation.parentTask, snapshot, resolvedOptions);
|
|
2431
|
+
if (!built.ok) {
|
|
2432
|
+
return scaleFailure(built.code, built.details);
|
|
2433
|
+
}
|
|
2434
|
+
const { context: ctx, space } = built;
|
|
2435
|
+
const solved = solveScale(ctx, space, targetDuration);
|
|
2436
|
+
const finalTasks = buildFinalTasks(ctx, solved.state, solved.layout, space);
|
|
2437
|
+
const externalPolicy = resolvedOptions.externalDependencyPolicy ?? "subtree-only";
|
|
2438
|
+
let externalChanged = [];
|
|
2439
|
+
if (externalPolicy === "subtree-only") {
|
|
2440
|
+
const violations = findOutgoingViolations(ctx, finalTasks);
|
|
2441
|
+
if (violations.length > 0) {
|
|
2442
|
+
return scaleFailure("EXTERNAL_DEPENDENCY_CONFLICT", violations);
|
|
2443
|
+
}
|
|
2444
|
+
} else {
|
|
2445
|
+
const cascaded = cascadeExternalSuccessors(ctx, finalTasks);
|
|
2446
|
+
if (!cascaded.ok) {
|
|
2447
|
+
return scaleFailure("EXTERNAL_DEPENDENCY_CONFLICT", cascaded.details);
|
|
2448
|
+
}
|
|
2449
|
+
externalChanged = cascaded.changedExternal;
|
|
2450
|
+
}
|
|
2451
|
+
const finalErrors = validateFinalGraph(ctx, finalTasks, externalChanged);
|
|
2452
|
+
if (finalErrors.length > 0) {
|
|
2453
|
+
return scaleFailure("INVALID_DEPENDENCIES", finalErrors);
|
|
2454
|
+
}
|
|
2455
|
+
const changedLeaves = ctx.leaves.filter((leaf) => finalTasks.has(leaf.id)).map((leaf) => finalTasks.get(leaf.id));
|
|
2456
|
+
const changedParents = [...ctx.nestedParents.map(({ task }) => task), ctx.parentTask].filter((task) => finalTasks.has(task.id)).map((task) => finalTasks.get(task.id));
|
|
2457
|
+
const changedTasks = [...changedLeaves, ...changedParents, ...externalChanged];
|
|
2458
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2459
|
+
const uniqueChanged = changedTasks.filter((task) => {
|
|
2460
|
+
if (seen.has(task.id)) return false;
|
|
2461
|
+
seen.add(task.id);
|
|
2462
|
+
return true;
|
|
2463
|
+
});
|
|
2464
|
+
const warnings = solved.clamped ? [
|
|
2465
|
+
{
|
|
2466
|
+
code: "TARGET_CLAMPED_TO_MINIMUM",
|
|
2467
|
+
requestedDuration: targetDuration,
|
|
2468
|
+
minimumDuration: solved.appliedSpan
|
|
2469
|
+
}
|
|
2470
|
+
] : [];
|
|
2471
|
+
return {
|
|
2472
|
+
ok: true,
|
|
2473
|
+
changedTasks: uniqueChanged,
|
|
2474
|
+
changedIds: uniqueChanged.map((task) => task.id),
|
|
2475
|
+
requestedDuration: targetDuration,
|
|
2476
|
+
appliedDuration: solved.appliedSpan,
|
|
2477
|
+
clamped: solved.clamped,
|
|
2478
|
+
warnings
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
1360
2481
|
export {
|
|
1361
2482
|
DAY_MS,
|
|
1362
2483
|
addBusinessDays,
|
|
@@ -1401,6 +2522,7 @@ export {
|
|
|
1401
2522
|
reflowTasksOnModeSwitch,
|
|
1402
2523
|
removeDependenciesBetweenTasks,
|
|
1403
2524
|
resizeTaskWithCascade,
|
|
2525
|
+
scaleTaskSubtreeDuration,
|
|
1404
2526
|
shiftBusinessDayOffset,
|
|
1405
2527
|
subtractBusinessDays,
|
|
1406
2528
|
universalCascade,
|