@critical-path/core 0.54.0 → 0.56.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/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/dist/domain/authorization.d.ts.map +1 -1
- package/dist/domain/authorization.js +3 -0
- package/dist/domain/authorization.js.map +1 -1
- package/dist/domain/cpm.d.ts +14 -3
- package/dist/domain/cpm.d.ts.map +1 -1
- package/dist/domain/cpm.js +146 -48
- package/dist/domain/cpm.js.map +1 -1
- package/dist/domain/effort.d.ts +17 -0
- package/dist/domain/effort.d.ts.map +1 -0
- package/dist/domain/effort.js +30 -0
- package/dist/domain/effort.js.map +1 -0
- package/dist/domain/events.d.ts +18 -2
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js +2 -0
- package/dist/domain/events.js.map +1 -1
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +1 -0
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/ladder.d.ts.map +1 -1
- package/dist/domain/ladder.js +33 -21
- package/dist/domain/ladder.js.map +1 -1
- package/dist/domain/metrics.d.ts.map +1 -1
- package/dist/domain/metrics.js +17 -3
- package/dist/domain/metrics.js.map +1 -1
- package/dist/domain/workload.d.ts.map +1 -1
- package/dist/domain/workload.js +3 -1
- package/dist/domain/workload.js.map +1 -1
- package/dist/engine/index.d.ts +14 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +125 -44
- package/dist/engine/index.js.map +1 -1
- package/dist/schemas/index.d.ts +292 -153
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +53 -21
- package/dist/schemas/index.js.map +1 -1
- package/dist/store/firebase.d.ts +56 -13
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +83 -71
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +11 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +2 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +15 -0
- package/dist/store/sqlite.js.map +1 -1
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +6 -0
- package/dist/testing/conformance.js.map +1 -1
- package/dist/types/index.d.ts +16 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/update.d.ts +7 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +16 -0
- package/dist/utils/update.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @critical-path/core
|
|
2
2
|
|
|
3
|
+
## 0.56.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 42ecdb9: Clearing fields over HTTP, validated dates, and correctable time entries.
|
|
8
|
+
|
|
9
|
+
- Update bodies accept `null` to remove an optional field (tasks, projects, deliverables, containers, iterations and time entries); parsing turns it into `undefined`, which every store treats as removal. Required fields (titles, names, task `status` and `priority`) still reject it. New `applyUpdate(record, updates)` applies payloads the same way for optimistic UI state; the React and Svelte task and deliverable state use it.
|
|
10
|
+
- **BREAKING:** every date field in request bodies must be an ISO date or date-time (`400` otherwise). Free-text dates used to be stored and then broke levelling, metrics and the ladder.
|
|
11
|
+
- Time entries can be corrected and deleted: `engine.getTimeEntry` / `updateTimeEntry` / `deleteTimeEntry`, `GET | PATCH | DELETE /time-entries/:id`, and the matching client methods. The task's hour totals move by the difference, and `time.updated` / `time.deleted` events are published. A new `time.manage` permission (project managers and admins) covers other people's entries; anyone with `time.log` can manage their own.
|
|
12
|
+
- `logTime` always requires the task to exist (it used to store orphan entries when no policy was enforced), and logs inside a transaction where the store supports one.
|
|
13
|
+
- **BREAKING (custom adapters):** `TimeEntryRepository` gains `getTimeEntry(id)` and `updateTimeEntry(id, updates)`.
|
|
14
|
+
- `FirebaseStore` updates every document inside `runTransaction` when the db supports it and implements `incrementTaskHours`, so concurrent updates and time logs are no longer lost. Pass Firestore's `FieldValue` as the new `fieldValue` option to add hour totals with `FieldValue.increment` (atomic, no contention) instead of transactions.
|
|
15
|
+
- MCP: new `update_time_entry` and `delete_time_entry` tools.
|
|
16
|
+
|
|
17
|
+
## 0.55.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 5dbf25b: Effort and progress roll-ups agree, count subtasks once and leave canceled work out.
|
|
22
|
+
|
|
23
|
+
- New `getTaskEstimateHours(task)` is the one place that reads an estimate (0 for milestones; an explicit 0 counts). Critical path analysis, workload, the ladder, metrics/EVM and deliverable summaries all use it, along with `getSummaryTaskIds` / `getLeafTasks`.
|
|
24
|
+
- **BREAKING:** a task with subtasks is a summary task. Critical path analysis schedules only leaf tasks: dependencies and deadlines on a summary apply to every leaf under it, and the summary is reported as a roll-up (`isSummary: true`; not in `criticalTaskIds`). Workload, the ladder and deliverable summaries count effort from leaves only, so an epic's estimate no longer doubles its subtasks'.
|
|
25
|
+
- **BREAKING:** canceled tasks leave estimate totals and progress denominators in the ladder and deliverable summaries (their logged hours still count), so a project can reach 100% and `completed`. Canceled tasks have zero planned and earned value in EVM. The ladder and deliverable summaries use inferred progress (`inferTaskProgress`) like task metrics.
|
|
26
|
+
- Unestimated tasks are listed in `unestimatedTaskIds`, and the hours assumed for them are configurable: `defaultEstimateHours` per call (`?defaultEstimateHours=` over HTTP, client and MCP options) or `criticalPathDefaultEstimateHours` on the engine. Default 1, as before.
|
|
27
|
+
|
|
3
28
|
## 0.54.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -365,7 +365,7 @@ console.log(leveled.unleveledProjectEndDate, '->', leveled.projectEndDate);
|
|
|
365
365
|
|
|
366
366
|
With `calendars: 'assignee'` (or `criticalPathCalendars: 'assignee'` on the engine), each task uses its assignee's schedule, then its team's, then the project calendar. Passes run on dates, slack is measured in each task's calendar, and each calendar is evaluated in its own `timezone` (IANA name; default UTC), including daylight-saving changes. `levelResources: true` additionally delays tasks so nobody works on two at once (priority rule `levelingPriority`, default least slack), reporting `levelingDelayHours` and `waitingOn` per task. Tasks with `allocation` below 1 take that share of their assignee (a 16h task at 0.5 spans 32 working hours); levelling fits them within 100% per person, and `overallocations` lists who is booked above 100% and when. Teams act as pools of `headcount` people (default: member count) shared by team-only tasks and members' own tasks. Finished tasks take no time. `calculatePortfolioCriticalPath({ projectIds })` levels several projects together, sharing people and teams across them; `includeHiddenWork` also counts unreadable projects' work as `'hidden'`. `effort: 'remaining'` schedules only what is left on in-progress tasks.
|
|
367
367
|
|
|
368
|
-
Tasks with `isMilestone: true` are zero-duration checkpoints (their estimates are ignored). A `deadline` on any task caps its late finish: a forecast that misses it shows as negative slack along the path driving it, with `deadlineDate` and `deadlineVarianceHours` per task and `missedDeadlineTaskIds` on the analysis. A date-only deadline means the end of that working day; project-calendar mode needs a start date to apply deadlines.
|
|
368
|
+
Tasks with subtasks are summary tasks, reported as roll-ups of their leaves (`isSummary`); dependencies and deadlines on them apply to every leaf. Unestimated tasks use `defaultEstimateHours` (default 1) and are listed in `unestimatedTaskIds`. Tasks with `isMilestone: true` are zero-duration checkpoints (their estimates are ignored). A `deadline` on any task caps its late finish: a forecast that misses it shows as negative slack along the path driving it, with `deadlineDate` and `deadlineVarianceHours` per task and `missedDeadlineTaskIds` on the analysis. A date-only deadline means the end of that working day; project-calendar mode needs a start date to apply deadlines.
|
|
369
369
|
|
|
370
370
|
### 9. Fractional Lexical Indexing (Kanban Reordering)
|
|
371
371
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["../../src/domain/authorization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,mBAAmB,EACnB,mBAAmB,EAEnB,WAAW,EACZ,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["../../src/domain/authorization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,mBAAmB,EACnB,mBAAmB,EAEnB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAuB3B,mDAAmD;AACnD,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,WAAW,EAAE,SAAS,mBAAmB,EAAE,CAKxF,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,mBAAmB,EAAE,CAAC,CAAC,CAAC;IAC/E,0GAA0G;IAC1G,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAC9C,8EAA8E;IAC9E,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;CAChD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,mBAAmB,CA8BrF"}
|
|
@@ -11,6 +11,7 @@ const PROJECT_MANAGER = [
|
|
|
11
11
|
...CONTRIBUTOR,
|
|
12
12
|
'task.delete',
|
|
13
13
|
'plan.manage',
|
|
14
|
+
'time.manage',
|
|
14
15
|
'project.update',
|
|
15
16
|
'project.manage_members',
|
|
16
17
|
'comment.moderate',
|
|
@@ -59,6 +60,8 @@ export function createRolePolicy(options = {}) {
|
|
|
59
60
|
return true;
|
|
60
61
|
if (action === 'attachment.delete' && ownsResource && granted.has('attachment.create'))
|
|
61
62
|
return true;
|
|
63
|
+
if (action === 'time.manage' && ownsResource && granted.has('time.log'))
|
|
64
|
+
return true;
|
|
62
65
|
return false;
|
|
63
66
|
};
|
|
64
67
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorization.js","sourceRoot":"","sources":["../../src/domain/authorization.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"authorization.js","sourceRoot":"","sources":["../../src/domain/authorization.ts"],"names":[],"mappings":"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"}
|
package/dist/domain/cpm.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { Task, TaskDependency, CriticalPathAnalysis, WorkSchedule, User, Team, PortfolioCriticalPathAnalysis } from '../types/index.js';
|
|
2
|
-
|
|
2
|
+
/** Hours assumed for a task with no estimate and no logged effort (see `CPMOptions.defaultEstimateHours`). */
|
|
3
|
+
export declare const DEFAULT_ESTIMATE_HOURS = 1;
|
|
4
|
+
/**
|
|
5
|
+
* Effort critical path analysis and workload plan for a task: its estimate (`getTaskEstimateHours`;
|
|
6
|
+
* 0 for milestones), else the effort logged on it, else `defaultEstimateHours`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getTaskDurationHours(task: Task, defaultEstimateHours?: number): number;
|
|
3
9
|
/** The task's `allocation`, clamped to (0, 1]; 1 when unset or invalid. */
|
|
4
10
|
export declare function getTaskAllocation(task: Task): number;
|
|
5
11
|
/** Working hours a task spans: its effort divided by its allocation. */
|
|
6
|
-
export declare function getTaskElapsedHours(task: Task): number;
|
|
12
|
+
export declare function getTaskElapsedHours(task: Task, defaultEstimateHours?: number): number;
|
|
7
13
|
/** True for completed or canceled tasks (by `semanticStatus`, else the default `done`/`canceled` statuses). */
|
|
8
14
|
export declare function isTaskFinished(task: Task): boolean;
|
|
9
15
|
/** What effort critical path analysis schedules: the full estimate, or what remains on in-progress tasks. */
|
|
@@ -13,7 +19,7 @@ export type EffortBasis = 'estimate' | 'remaining';
|
|
|
13
19
|
* more time and occupies nobody), otherwise its effort divided by its allocation. With
|
|
14
20
|
* `effort: 'remaining'`, an in-progress task's effort is `max(0, estimate - loggedHours)`.
|
|
15
21
|
*/
|
|
16
|
-
export declare function getTaskScheduledHours(task: Task, effort?: EffortBasis): number;
|
|
22
|
+
export declare function getTaskScheduledHours(task: Task, effort?: EffortBasis, defaultEstimateHours?: number): number;
|
|
17
23
|
export interface CPMOptions {
|
|
18
24
|
projectStartDate?: string | Date;
|
|
19
25
|
/** The project calendar. Default: `DEFAULT_WORK_SCHEDULE`. */
|
|
@@ -47,6 +53,11 @@ export interface CPMOptions {
|
|
|
47
53
|
* no time either way.
|
|
48
54
|
*/
|
|
49
55
|
effort?: EffortBasis;
|
|
56
|
+
/**
|
|
57
|
+
* Hours assumed for an unfinished task with no estimate and no logged effort. Default 1. Such
|
|
58
|
+
* tasks are listed in `unestimatedTaskIds`, so a half-estimated plan does not look certain.
|
|
59
|
+
*/
|
|
60
|
+
defaultEstimateHours?: number;
|
|
50
61
|
}
|
|
51
62
|
export type LevelingPriority = 'slack' | 'priority' | 'dueDate' | 'order';
|
|
52
63
|
/** The calendar a task runs on in assignee mode: assignee, then task team, then project. */
|
package/dist/domain/cpm.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cpm.d.ts","sourceRoot":"","sources":["../../src/domain/cpm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,cAAc,EAEd,oBAAoB,EACpB,YAAY,EACZ,IAAI,EACJ,IAAI,EAEJ,6BAA6B,EAC9B,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"cpm.d.ts","sourceRoot":"","sources":["../../src/domain/cpm.ts"],"names":[],"mappings":"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"}
|
package/dist/domain/cpm.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { DEFAULT_WORK_SCHEDULE, addWorkingHours, subtractWorkingHours, getWorkingHoursBetween, nextWorkingTime, previousWorkingTime, parseDate, toDateString } from './calendar.js';
|
|
2
2
|
import { isTaskInProgress } from '../utils/status.js';
|
|
3
3
|
import { precedenceOf } from './graph.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
import { getTaskEstimateHours, getSummaryTaskIds } from './effort.js';
|
|
5
|
+
/** Hours assumed for a task with no estimate and no logged effort (see `CPMOptions.defaultEstimateHours`). */
|
|
6
|
+
export const DEFAULT_ESTIMATE_HOURS = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Effort critical path analysis and workload plan for a task: its estimate (`getTaskEstimateHours`;
|
|
9
|
+
* 0 for milestones), else the effort logged on it, else `defaultEstimateHours`.
|
|
10
|
+
*/
|
|
11
|
+
export function getTaskDurationHours(task, defaultEstimateHours = DEFAULT_ESTIMATE_HOURS) {
|
|
12
|
+
const estimate = getTaskEstimateHours(task);
|
|
13
|
+
if (estimate !== undefined)
|
|
14
|
+
return estimate;
|
|
15
|
+
if (typeof task.actualHours === 'number' && task.actualHours > 0)
|
|
15
16
|
return task.actualHours;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
return defaultEstimateHours;
|
|
18
|
+
}
|
|
19
|
+
/** True when `getTaskDurationHours` falls back to the default: no estimate and no logged effort. */
|
|
20
|
+
function isUnestimated(task) {
|
|
21
|
+
return getTaskEstimateHours(task) === undefined && !(typeof task.actualHours === 'number' && task.actualHours > 0);
|
|
19
22
|
}
|
|
20
23
|
/** The task's `allocation`, clamped to (0, 1]; 1 when unset or invalid. */
|
|
21
24
|
export function getTaskAllocation(task) {
|
|
@@ -23,8 +26,8 @@ export function getTaskAllocation(task) {
|
|
|
23
26
|
return typeof a === 'number' && a > 0 && a <= 1 ? a : 1;
|
|
24
27
|
}
|
|
25
28
|
/** Working hours a task spans: its effort divided by its allocation. */
|
|
26
|
-
export function getTaskElapsedHours(task) {
|
|
27
|
-
return getTaskDurationHours(task) / getTaskAllocation(task);
|
|
29
|
+
export function getTaskElapsedHours(task, defaultEstimateHours) {
|
|
30
|
+
return getTaskDurationHours(task, defaultEstimateHours) / getTaskAllocation(task);
|
|
28
31
|
}
|
|
29
32
|
/** True for completed or canceled tasks (by `semanticStatus`, else the default `done`/`canceled` statuses). */
|
|
30
33
|
export function isTaskFinished(task) {
|
|
@@ -37,13 +40,13 @@ export function isTaskFinished(task) {
|
|
|
37
40
|
* more time and occupies nobody), otherwise its effort divided by its allocation. With
|
|
38
41
|
* `effort: 'remaining'`, an in-progress task's effort is `max(0, estimate - loggedHours)`.
|
|
39
42
|
*/
|
|
40
|
-
export function getTaskScheduledHours(task, effort = 'estimate') {
|
|
43
|
+
export function getTaskScheduledHours(task, effort = 'estimate', defaultEstimateHours) {
|
|
41
44
|
if (isTaskFinished(task))
|
|
42
45
|
return 0;
|
|
43
46
|
if (effort === 'remaining' && isTaskInProgress({ status: task.status, semanticStatus: task.semanticStatus })) {
|
|
44
|
-
return Math.max(0, getTaskDurationHours(task) - (task.loggedHours ?? 0)) / getTaskAllocation(task);
|
|
47
|
+
return Math.max(0, getTaskDurationHours(task, defaultEstimateHours) - (task.loggedHours ?? 0)) / getTaskAllocation(task);
|
|
45
48
|
}
|
|
46
|
-
return getTaskElapsedHours(task);
|
|
49
|
+
return getTaskElapsedHours(task, defaultEstimateHours);
|
|
47
50
|
}
|
|
48
51
|
/** The calendar a task runs on in assignee mode: assignee, then task team, then project. */
|
|
49
52
|
export function resolveTaskSchedule(task, context) {
|
|
@@ -71,8 +74,35 @@ const milestoneFields = (task, deadline, varianceHours) => ({
|
|
|
71
74
|
...(deadline ? { deadlineDate: deadline.toISOString(), deadlineVarianceHours: round2(varianceHours) } : {})
|
|
72
75
|
});
|
|
73
76
|
const allocationField = (task) => (getTaskAllocation(task) < 1 ? { allocation: getTaskAllocation(task) } : {});
|
|
77
|
+
const deadlinesOf = (graph, id) => graph.deadlines.get(id) ?? [];
|
|
78
|
+
/**
|
|
79
|
+
* The scheduling graph. Only leaf tasks are scheduled: a summary task (one with subtasks among
|
|
80
|
+
* `tasks`) spans its subtasks, so a dependency on it applies to every leaf under it, and its
|
|
81
|
+
* deadline caps those leaves. Summaries are reported afterwards as roll-ups (`addSummaries`).
|
|
82
|
+
*/
|
|
74
83
|
function buildGraph(tasks, dependencies) {
|
|
75
|
-
const
|
|
84
|
+
const summaryIds = getSummaryTaskIds(tasks);
|
|
85
|
+
const all = new Map(tasks.map((t) => [t.id, t]));
|
|
86
|
+
const childrenOf = new Map();
|
|
87
|
+
for (const t of tasks) {
|
|
88
|
+
if (t.parentId && summaryIds.has(t.parentId))
|
|
89
|
+
childrenOf.set(t.parentId, [...(childrenOf.get(t.parentId) ?? []), t.id]);
|
|
90
|
+
}
|
|
91
|
+
const leavesOf = (id, seen = new Set()) => {
|
|
92
|
+
if (!summaryIds.has(id) || seen.has(id))
|
|
93
|
+
return [id];
|
|
94
|
+
seen.add(id);
|
|
95
|
+
return childrenOf.get(id).flatMap((child) => leavesOf(child, seen));
|
|
96
|
+
};
|
|
97
|
+
const summaries = new Map([...summaryIds].map((id) => [id, { task: all.get(id), leafIds: leavesOf(id) }]));
|
|
98
|
+
const deadlines = new Map();
|
|
99
|
+
for (const t of tasks) {
|
|
100
|
+
if (!t.deadline)
|
|
101
|
+
continue;
|
|
102
|
+
for (const leaf of leavesOf(t.id))
|
|
103
|
+
deadlines.set(leaf, [...(deadlines.get(leaf) ?? []), t.deadline]);
|
|
104
|
+
}
|
|
105
|
+
const taskMap = new Map(tasks.filter((t) => !summaryIds.has(t.id)).map((t) => [t.id, t]));
|
|
76
106
|
const taskIds = Array.from(taskMap.keys());
|
|
77
107
|
// Build adjacency graph:
|
|
78
108
|
// prerequisites[v] = tasks that v depends on (must finish before v starts)
|
|
@@ -85,9 +115,15 @@ function buildGraph(tasks, dependencies) {
|
|
|
85
115
|
}
|
|
86
116
|
for (const dep of dependencies) {
|
|
87
117
|
const edge = precedenceOf(dep);
|
|
88
|
-
if (edge
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
if (!edge || !all.has(edge.before) || !all.has(edge.after))
|
|
119
|
+
continue;
|
|
120
|
+
for (const before of leavesOf(edge.before)) {
|
|
121
|
+
for (const after of leavesOf(edge.after)) {
|
|
122
|
+
if (before === after)
|
|
123
|
+
continue;
|
|
124
|
+
prerequisites.get(after).add(before);
|
|
125
|
+
successors.get(before).add(after);
|
|
126
|
+
}
|
|
91
127
|
}
|
|
92
128
|
}
|
|
93
129
|
// Topological sorting via Kahn's algorithm
|
|
@@ -118,7 +154,58 @@ function buildGraph(tasks, dependencies) {
|
|
|
118
154
|
const sorted = new Set(topoOrder);
|
|
119
155
|
const cyclicTaskIds = taskIds.filter((id) => !sorted.has(id));
|
|
120
156
|
topoOrder.push(...cyclicTaskIds);
|
|
121
|
-
return { taskMap, taskIds, prerequisites, successors, topoOrder, cyclicTaskIds };
|
|
157
|
+
return { taskMap, taskIds, prerequisites, successors, topoOrder, cyclicTaskIds, summaries, deadlines };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Adds each summary task to `analysis` as a roll-up of the leaf tasks under it: earliest start,
|
|
161
|
+
* latest finish, least slack, critical when any leaf is. Summaries are not added to
|
|
162
|
+
* `criticalTaskIds`, whose tasks are the scheduled (leaf) ones. A summary's own deadline is
|
|
163
|
+
* reported against its latest finish.
|
|
164
|
+
*/
|
|
165
|
+
function addSummaries(analysis, graph, schedule) {
|
|
166
|
+
const byId = new Map(analysis.tasks.map((t) => [t.taskId, t]));
|
|
167
|
+
const rollups = [];
|
|
168
|
+
const missed = [];
|
|
169
|
+
for (const [id, { task, leafIds }] of graph.summaries) {
|
|
170
|
+
const leaves = leafIds.map((leaf) => byId.get(leaf)).filter((t) => Boolean(t));
|
|
171
|
+
if (!leaves.length)
|
|
172
|
+
continue;
|
|
173
|
+
const min = (pick) => Math.min(...leaves.map(pick));
|
|
174
|
+
const max = (pick) => Math.max(...leaves.map(pick));
|
|
175
|
+
const dates = (pick) => leaves.map(pick).filter((d) => Boolean(d)).sort();
|
|
176
|
+
const [starts, finishes, lateStarts, lateFinishes] = [dates((t) => t.earlyStartDate), dates((t) => t.earlyFinishDate), dates((t) => t.lateStartDate), dates((t) => t.lateFinishDate)];
|
|
177
|
+
const slack = min((t) => t.totalSlack);
|
|
178
|
+
const earlyFinishDate = finishes[finishes.length - 1];
|
|
179
|
+
const deadline = task.deadline && earlyFinishDate ? getDeadlineInstant(task.deadline, schedule) : undefined;
|
|
180
|
+
const variance = deadline ? slackHours(new Date(earlyFinishDate), deadline, schedule) : undefined;
|
|
181
|
+
if (variance !== undefined && variance < -0.001)
|
|
182
|
+
missed.push(id);
|
|
183
|
+
rollups.push({
|
|
184
|
+
taskId: id,
|
|
185
|
+
isSummary: true,
|
|
186
|
+
earlyStart: min((t) => t.earlyStart),
|
|
187
|
+
earlyFinish: max((t) => t.earlyFinish),
|
|
188
|
+
lateStart: min((t) => t.lateStart),
|
|
189
|
+
lateFinish: max((t) => t.lateFinish),
|
|
190
|
+
totalSlack: slack,
|
|
191
|
+
slackWorkingHours: slack,
|
|
192
|
+
isCritical: leaves.some((t) => t.isCritical),
|
|
193
|
+
durationHours: round2(max((t) => t.earlyFinish) - min((t) => t.earlyStart)),
|
|
194
|
+
...(starts.length ? { earlyStartDate: starts[0], earlyFinishDate, lateStartDate: lateStarts[0], lateFinishDate: lateFinishes[lateFinishes.length - 1] } : {}),
|
|
195
|
+
...milestoneFields({ ...task, isMilestone: false }, deadline, variance)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (!rollups.length)
|
|
199
|
+
return analysis;
|
|
200
|
+
return { ...analysis, tasks: [...analysis.tasks, ...rollups], missedDeadlineTaskIds: [...analysis.missedDeadlineTaskIds, ...missed] };
|
|
201
|
+
}
|
|
202
|
+
/** Completes a project's analysis: unestimated tasks and summary roll-ups. */
|
|
203
|
+
function finishAnalysis(analysis, graph, schedule, taskIds) {
|
|
204
|
+
const unestimatedTaskIds = taskIds.filter((id) => {
|
|
205
|
+
const task = graph.taskMap.get(id);
|
|
206
|
+
return !isTaskFinished(task) && isUnestimated(task);
|
|
207
|
+
});
|
|
208
|
+
return addSummaries({ ...analysis, ...(unestimatedTaskIds.length ? { unestimatedTaskIds } : {}) }, graph, schedule);
|
|
122
209
|
}
|
|
123
210
|
/**
|
|
124
211
|
* Assignee-calendar CPM. Passes run on dates, because working-hour offsets mean different
|
|
@@ -160,12 +247,17 @@ function scheduleOnDates(graph, segments, options, calculatedAt) {
|
|
|
160
247
|
const task = taskMap.get(id);
|
|
161
248
|
return [id, options.calendars === 'assignee' ? resolveTaskSchedule(task, { ...options, schedule: seg.schedule }) : seg.schedule];
|
|
162
249
|
}));
|
|
163
|
-
const durationOf = new Map(taskIds.map((id) => [id, getTaskScheduledHours(taskMap.get(id), options.effort)]));
|
|
250
|
+
const durationOf = new Map(taskIds.map((id) => [id, getTaskScheduledHours(taskMap.get(id), options.effort, options.defaultEstimateHours)]));
|
|
251
|
+
// A task's late finish is capped by its own deadline and by those of the summary tasks above it
|
|
164
252
|
const deadlineOf = new Map(taskIds.flatMap((id) => {
|
|
165
|
-
const
|
|
166
|
-
return
|
|
253
|
+
const instants = deadlinesOf(graph, id).map((d) => getDeadlineInstant(d, calendarOf.get(id)));
|
|
254
|
+
return instants.length ? [[id, new Date(Math.min(...instants.map((d) => d.getTime())))]] : [];
|
|
167
255
|
}));
|
|
168
|
-
const
|
|
256
|
+
const ownDeadlineOf = (id) => {
|
|
257
|
+
const deadline = taskMap.get(id).deadline;
|
|
258
|
+
return deadline ? getDeadlineInstant(deadline, calendarOf.get(id)) : undefined;
|
|
259
|
+
};
|
|
260
|
+
const ctx = { taskMap, calendarOf, durationOf, deadlineOf, ownDeadlineOf, startOf: (id) => segmentOf.get(id).start };
|
|
169
261
|
const endOfProjects = (dates) => {
|
|
170
262
|
const ends = new Map(segments.map((seg) => [seg.projectId, latest(seg.taskIds.map((id) => dates.finish.get(id)), seg.start)]));
|
|
171
263
|
return (id) => ends.get(segmentOf.get(id).projectId);
|
|
@@ -176,7 +268,7 @@ function scheduleOnDates(graph, segments, options, calculatedAt) {
|
|
|
176
268
|
const cyclic = new Set(graph.cyclicTaskIds);
|
|
177
269
|
const analyse = (seg, e, l, extra) => {
|
|
178
270
|
const cyclicTaskIds = seg.taskIds.filter((id) => cyclic.has(id));
|
|
179
|
-
return { ...buildAnalysis(seg, calculatedAt, e, l, ctx, extra), ...(cyclicTaskIds.length ? { cyclicTaskIds } : {}) };
|
|
271
|
+
return finishAnalysis({ ...buildAnalysis(seg, calculatedAt, e, l, ctx, extra), ...(cyclicTaskIds.length ? { cyclicTaskIds } : {}) }, graph, seg.schedule, seg.taskIds);
|
|
180
272
|
};
|
|
181
273
|
if (!options.levelResources) {
|
|
182
274
|
const overallocations = findOverallocations(taskMap, early, resourcesOf);
|
|
@@ -490,7 +582,7 @@ function findOverallocations(taskMap, dates, resourcesOf) {
|
|
|
490
582
|
return result;
|
|
491
583
|
}
|
|
492
584
|
function buildAnalysis({ projectId, start: projectStart, schedule: projectSchedule, taskIds }, calculatedAt, early, late, ctx, extra = () => ({})) {
|
|
493
|
-
const { calendarOf, durationOf,
|
|
585
|
+
const { calendarOf, durationOf, ownDeadlineOf, taskMap } = ctx;
|
|
494
586
|
const offset = (date) => date >= projectStart
|
|
495
587
|
? getWorkingHoursBetween(projectStart, date, projectSchedule)
|
|
496
588
|
: -getWorkingHoursBetween(date, projectStart, projectSchedule);
|
|
@@ -520,7 +612,7 @@ function buildAnalysis({ projectId, start: projectStart, schedule: projectSchedu
|
|
|
520
612
|
lateFinishDate: lf.toISOString(),
|
|
521
613
|
...(calendar.id ? { scheduleId: calendar.id } : {}),
|
|
522
614
|
...allocationField(taskMap.get(id)),
|
|
523
|
-
...milestoneFields(taskMap.get(id),
|
|
615
|
+
...milestoneFields(taskMap.get(id), ownDeadlineOf(id), ownDeadlineOf(id) ? slackHours(ef, ownDeadlineOf(id), calendar) : undefined),
|
|
524
616
|
...extra(id)
|
|
525
617
|
});
|
|
526
618
|
}
|
|
@@ -555,7 +647,8 @@ export function calculatePortfolioCPM(projects, dependencies, options = {}) {
|
|
|
555
647
|
projectId: p.projectId,
|
|
556
648
|
start: startOrToday(p.projectStartDate),
|
|
557
649
|
schedule: p.schedule || DEFAULT_WORK_SCHEDULE,
|
|
558
|
-
|
|
650
|
+
// Summary tasks are not scheduled; they are added back as roll-ups
|
|
651
|
+
taskIds: p.tasks.map((t) => t.id).filter((id) => graph.taskMap.has(id))
|
|
559
652
|
}));
|
|
560
653
|
const result = scheduleOnDates(graph, segments, options, calculatedAt);
|
|
561
654
|
// Background projects are dropped from the results; hidden ones also have their task ids redacted.
|
|
@@ -614,16 +707,21 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
614
707
|
return calculateCalendarCPM(projectId, calculatedAt, graph, options);
|
|
615
708
|
}
|
|
616
709
|
// Deadlines as project-calendar hours from the start. Without a start date there is nothing to
|
|
617
|
-
// place them against, so they are ignored.
|
|
618
|
-
|
|
710
|
+
// place them against, so they are ignored. A task's late finish is capped by its own deadline and
|
|
711
|
+
// by those of the summary tasks above it; its schedule reports its own.
|
|
712
|
+
const capHours = new Map();
|
|
713
|
+
const ownDeadline = new Map();
|
|
714
|
+
const toHours = (deadline) => deadline >= projectStartDate
|
|
715
|
+
? getWorkingHoursBetween(projectStartDate, deadline, schedule)
|
|
716
|
+
: -getWorkingHoursBetween(deadline, projectStartDate, schedule);
|
|
619
717
|
if (projectStartDate) {
|
|
620
|
-
for (const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
718
|
+
for (const id of taskIds) {
|
|
719
|
+
const caps = deadlinesOf(graph, id).map((d) => toHours(getDeadlineInstant(d, schedule)));
|
|
720
|
+
if (caps.length)
|
|
721
|
+
capHours.set(id, Math.min(...caps));
|
|
722
|
+
const own = taskMap.get(id).deadline;
|
|
723
|
+
if (own)
|
|
724
|
+
ownDeadline.set(id, getDeadlineInstant(own, schedule));
|
|
627
725
|
}
|
|
628
726
|
}
|
|
629
727
|
// --- FORWARD PASS ---
|
|
@@ -631,7 +729,7 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
631
729
|
const earlyFinish = new Map();
|
|
632
730
|
for (const id of topoOrder) {
|
|
633
731
|
const task = taskMap.get(id);
|
|
634
|
-
const duration = getTaskScheduledHours(task, options.effort);
|
|
732
|
+
const duration = getTaskScheduledHours(task, options.effort, options.defaultEstimateHours);
|
|
635
733
|
const prereqs = prerequisites.get(id) || new Set();
|
|
636
734
|
let maxPrereqFinish = 0;
|
|
637
735
|
for (const p of prereqs) {
|
|
@@ -659,7 +757,7 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
659
757
|
for (let i = topoOrder.length - 1; i >= 0; i--) {
|
|
660
758
|
const id = topoOrder[i];
|
|
661
759
|
const task = taskMap.get(id);
|
|
662
|
-
const duration = getTaskScheduledHours(task, options.effort);
|
|
760
|
+
const duration = getTaskScheduledHours(task, options.effort, options.defaultEstimateHours);
|
|
663
761
|
const succs = successors.get(id) || new Set();
|
|
664
762
|
let minSuccLateStart = totalDurationHours;
|
|
665
763
|
if (succs.size > 0) {
|
|
@@ -674,7 +772,7 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
674
772
|
minSuccLateStart = totalDurationHours;
|
|
675
773
|
}
|
|
676
774
|
}
|
|
677
|
-
const lf = Math.min(minSuccLateStart,
|
|
775
|
+
const lf = Math.min(minSuccLateStart, capHours.get(id) ?? Infinity);
|
|
678
776
|
const ls = lf - duration;
|
|
679
777
|
lateFinish.set(id, lf);
|
|
680
778
|
lateStart.set(id, ls);
|
|
@@ -694,7 +792,7 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
694
792
|
criticalTaskIds.push(id);
|
|
695
793
|
}
|
|
696
794
|
const task = taskMap.get(id);
|
|
697
|
-
const duration = getTaskScheduledHours(task, options.effort);
|
|
795
|
+
const duration = getTaskScheduledHours(task, options.effort, options.defaultEstimateHours);
|
|
698
796
|
const earlyStartDate = projectStartDate
|
|
699
797
|
? addWorkingHours(projectStartDate, es, schedule).toISOString()
|
|
700
798
|
: undefined;
|
|
@@ -717,7 +815,7 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
717
815
|
isCritical,
|
|
718
816
|
durationHours: duration,
|
|
719
817
|
...allocationField(task),
|
|
720
|
-
...milestoneFields(task,
|
|
818
|
+
...milestoneFields(task, ownDeadline.get(id), ownDeadline.has(id) ? toHours(ownDeadline.get(id)) - ef : undefined),
|
|
721
819
|
slackWorkingHours: totalSlack,
|
|
722
820
|
earlyStartDate,
|
|
723
821
|
earlyFinishDate,
|
|
@@ -728,7 +826,7 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
728
826
|
const projectEndDate = projectStartDate
|
|
729
827
|
? addWorkingHours(projectStartDate, totalDurationHours, schedule).toISOString()
|
|
730
828
|
: undefined;
|
|
731
|
-
return {
|
|
829
|
+
return finishAnalysis({
|
|
732
830
|
projectId,
|
|
733
831
|
calculatedAt,
|
|
734
832
|
totalDurationHours: Math.round(totalDurationHours * 100) / 100,
|
|
@@ -739,6 +837,6 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
739
837
|
missedDeadlineTaskIds: missedDeadlines(schedules),
|
|
740
838
|
...(graph.cyclicTaskIds.length ? { cyclicTaskIds: graph.cyclicTaskIds } : {}),
|
|
741
839
|
tasks: schedules
|
|
742
|
-
};
|
|
840
|
+
}, graph, schedule, taskIds);
|
|
743
841
|
}
|
|
744
842
|
//# sourceMappingURL=cpm.js.map
|