@critical-path/core 0.55.0 → 0.57.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 +26 -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.map +1 -1
- package/dist/domain/cpm.js +111 -158
- package/dist/domain/cpm.js.map +1 -1
- 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/engine/index.d.ts +15 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +104 -29
- package/dist/engine/index.js.map +1 -1
- package/dist/schemas/index.d.ts +301 -153
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +55 -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,31 @@
|
|
|
1
1
|
# @critical-path/core
|
|
2
2
|
|
|
3
|
+
## 0.57.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 224ad51: One scheduling engine, start constraints and dependency lag.
|
|
8
|
+
|
|
9
|
+
- **BREAKING:** project-calendar critical path analysis now runs on the same date-based passes as assignee mode (every task on the project calendar), so the two cannot drift. Offsets are unchanged; dates now show a successor starting at the next working moment after its predecessor finishes (09:00 the next morning rather than 17:00), and results include `overallocations` and `scheduleId` as in assignee mode. Without a start date the analysis has offsets only, as before.
|
|
10
|
+
- New `Task.earliestStart`: start no earlier than this date (a date-only value is the start of that day). Applies in critical path analysis and levelling, and on a summary task to every leaf under it.
|
|
11
|
+
- New `TaskDependency.lagHours`: working hours to wait after the predecessor finishes (negative for a lead), accepted by `addDependency`, `POST /tasks/:id/dependencies` and MCP `add_dependency`. Links that differ only in lag are separate dependencies; the longer wait applies.
|
|
12
|
+
- Levelling orders its backward pass topologically over dependency and resource links, so leads cannot break slack.
|
|
13
|
+
- MCP `create_task` / `update_task` accept `earliestStart`.
|
|
14
|
+
|
|
15
|
+
## 0.56.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 42ecdb9: Clearing fields over HTTP, validated dates, and correctable time entries.
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
- **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.
|
|
23
|
+
- 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.
|
|
24
|
+
- `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.
|
|
25
|
+
- **BREAKING (custom adapters):** `TimeEntryRepository` gains `getTimeEntry(id)` and `updateTimeEntry(id, updates)`.
|
|
26
|
+
- `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.
|
|
27
|
+
- MCP: new `update_time_entry` and `delete_time_entry` tools.
|
|
28
|
+
|
|
3
29
|
## 0.55.0
|
|
4
30
|
|
|
5
31
|
### 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 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;
|
|
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. `earliestStart` (start no earlier than) and dependency `lagHours` (wait after the predecessor; negative for a lead) constrain the forward pass. Project-calendar mode needs a start date to apply these date constraints.
|
|
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":"AAQA,MAAM,MAAM,GAA0B,CAAC,cAAc,CAAC,CAAC;AACvD,MAAM,WAAW,GAA0B;IACzC,GAAG,MAAM;IACT,aAAa;IACb,aAAa;IACb,gBAAgB;IAChB,mBAAmB;IACnB,UAAU;CACX,CAAC;AACF,MAAM,eAAe,GAA0B;IAC7C,GAAG,WAAW;IACd,aAAa;IACb,aAAa;IACb,aAAa;IACb,gBAAgB;IAChB,wBAAwB;IACxB,kBAAkB;IAClB,mBAAmB;CACpB,CAAC;AACF,MAAM,aAAa,GAA0B,CAAC,GAAG,eAAe,EAAE,gBAAgB,EAAE,uBAAuB,CAAC,CAAC;AAE7G,mDAAmD;AACnD,MAAM,CAAC,MAAM,wBAAwB,GAAwD;IAC3F,MAAM,EAAE,MAAM;IACd,WAAW,EAAE,WAAW;IACxB,eAAe,EAAE,eAAe;IAChC,KAAK,EAAE,aAAa;CACrB,CAAC;AAgBF;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAA6B,EAAE;IAC9D,MAAM,WAAW,GAAG,EAAE,GAAG,wBAAwB,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAChF,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3D,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAClG,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,CAAC,CAAC,KAAY,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC;IACxG,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,WAAW,CAAC;IAErE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAwB,EAAW,EAAE;QACtF,IAAI,WAAW,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,MAAM,KAAK,gBAAgB;YAAE,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACjE,IAAI,MAAM,KAAK,kBAAkB;YAAE,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACpE,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAE3B,8DAA8D;QAC9D,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;aAClC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;aAChG,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAErC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1E,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;QAErC,8EAA8E;QAC9E,MAAM,YAAY,GAAG,QAAQ,EAAE,OAAO,KAAK,SAAS,IAAI,QAAQ,CAAC,OAAO,KAAK,KAAK,CAAC,MAAM,CAAC;QAC1F,IAAI,MAAM,KAAK,kBAAkB,IAAI,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;YAAE,OAAO,IAAI,CAAC;QAChG,IAAI,MAAM,KAAK,mBAAmB,IAAI,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;YAAE,OAAO,IAAI,CAAC;QACpG,IAAI,MAAM,KAAK,aAAa,IAAI,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;YAAE,OAAO,IAAI,CAAC;QAErF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
|
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;AAe3B,8GAA8G;AAC9G,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,oBAAoB,GAAE,MAA+B,GAAG,MAAM,CAK9G;AAOD,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAGpD;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,oBAAoB,CAAC,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAGlD;AAED,6GAA6G;AAC7G,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,WAAW,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,GAAE,WAAwB,EAAE,oBAAoB,CAAC,EAAE,MAAM,GAAG,MAAM,CAMzH;AAED,MAAM,WAAW,UAAU;IACzB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IACnC,iFAAiF;IACjF,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,kFAAkF;IAClF,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1E,4FAA4F;AAC5F,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;CAAE,GACnE,YAAY,CAOd;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,YAAoC,GAAG,IAAI,CAKzG;
|
|
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
|
@@ -74,6 +74,7 @@ const milestoneFields = (task, deadline, varianceHours) => ({
|
|
|
74
74
|
...(deadline ? { deadlineDate: deadline.toISOString(), deadlineVarianceHours: round2(varianceHours) } : {})
|
|
75
75
|
});
|
|
76
76
|
const allocationField = (task) => (getTaskAllocation(task) < 1 ? { allocation: getTaskAllocation(task) } : {});
|
|
77
|
+
const edgeKey = (before, after) => `${before}\u0000${after}`;
|
|
77
78
|
const deadlinesOf = (graph, id) => graph.deadlines.get(id) ?? [];
|
|
78
79
|
/**
|
|
79
80
|
* The scheduling graph. Only leaf tasks are scheduled: a summary task (one with subtasks among
|
|
@@ -96,12 +97,14 @@ function buildGraph(tasks, dependencies) {
|
|
|
96
97
|
};
|
|
97
98
|
const summaries = new Map([...summaryIds].map((id) => [id, { task: all.get(id), leafIds: leavesOf(id) }]));
|
|
98
99
|
const deadlines = new Map();
|
|
100
|
+
const earliestStarts = new Map();
|
|
99
101
|
for (const t of tasks) {
|
|
100
|
-
|
|
101
|
-
continue;
|
|
102
|
-
for (const leaf of leavesOf(t.id))
|
|
102
|
+
for (const leaf of t.deadline ? leavesOf(t.id) : [])
|
|
103
103
|
deadlines.set(leaf, [...(deadlines.get(leaf) ?? []), t.deadline]);
|
|
104
|
+
for (const leaf of t.earliestStart ? leavesOf(t.id) : [])
|
|
105
|
+
earliestStarts.set(leaf, [...(earliestStarts.get(leaf) ?? []), t.earliestStart]);
|
|
104
106
|
}
|
|
107
|
+
const lags = new Map();
|
|
105
108
|
const taskMap = new Map(tasks.filter((t) => !summaryIds.has(t.id)).map((t) => [t.id, t]));
|
|
106
109
|
const taskIds = Array.from(taskMap.keys());
|
|
107
110
|
// Build adjacency graph:
|
|
@@ -117,12 +120,17 @@ function buildGraph(tasks, dependencies) {
|
|
|
117
120
|
const edge = precedenceOf(dep);
|
|
118
121
|
if (!edge || !all.has(edge.before) || !all.has(edge.after))
|
|
119
122
|
continue;
|
|
123
|
+
const lag = Number.isFinite(dep.lagHours) ? dep.lagHours : 0;
|
|
120
124
|
for (const before of leavesOf(edge.before)) {
|
|
121
125
|
for (const after of leavesOf(edge.after)) {
|
|
122
126
|
if (before === after)
|
|
123
127
|
continue;
|
|
124
128
|
prerequisites.get(after).add(before);
|
|
125
129
|
successors.get(before).add(after);
|
|
130
|
+
// Two links between the same tasks: the longer wait wins
|
|
131
|
+
const key = edgeKey(before, after);
|
|
132
|
+
if (lag !== 0 || lags.has(key))
|
|
133
|
+
lags.set(key, Math.max(lags.get(key) ?? -Infinity, lag));
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
136
|
}
|
|
@@ -154,7 +162,7 @@ function buildGraph(tasks, dependencies) {
|
|
|
154
162
|
const sorted = new Set(topoOrder);
|
|
155
163
|
const cyclicTaskIds = taskIds.filter((id) => !sorted.has(id));
|
|
156
164
|
topoOrder.push(...cyclicTaskIds);
|
|
157
|
-
return { taskMap, taskIds, prerequisites, successors, topoOrder, cyclicTaskIds, summaries, deadlines };
|
|
165
|
+
return { taskMap, taskIds, prerequisites, successors, topoOrder, cyclicTaskIds, summaries, deadlines, earliestStarts, lags };
|
|
158
166
|
}
|
|
159
167
|
/**
|
|
160
168
|
* Adds each summary task to `analysis` as a roll-up of the leaf tasks under it: earliest start,
|
|
@@ -257,7 +265,21 @@ function scheduleOnDates(graph, segments, options, calculatedAt) {
|
|
|
257
265
|
const deadline = taskMap.get(id).deadline;
|
|
258
266
|
return deadline ? getDeadlineInstant(deadline, calendarOf.get(id)) : undefined;
|
|
259
267
|
};
|
|
260
|
-
|
|
268
|
+
// A start constraint means "not before": the first working moment at or after it
|
|
269
|
+
const notBefore = new Map(taskIds.flatMap((id) => {
|
|
270
|
+
const instants = (graph.earliestStarts.get(id) ?? []).map((d) => nextWorkingTime(d, calendarOf.get(id)));
|
|
271
|
+
return instants.length ? [[id, new Date(Math.max(...instants.map((d) => d.getTime())))]] : [];
|
|
272
|
+
}));
|
|
273
|
+
const ctx = {
|
|
274
|
+
taskMap,
|
|
275
|
+
calendarOf,
|
|
276
|
+
durationOf,
|
|
277
|
+
deadlineOf,
|
|
278
|
+
ownDeadlineOf,
|
|
279
|
+
notBefore,
|
|
280
|
+
lagOf: (before, after) => graph.lags.get(edgeKey(before, after)) ?? 0,
|
|
281
|
+
startOf: (id) => segmentOf.get(id).start
|
|
282
|
+
};
|
|
261
283
|
const endOfProjects = (dates) => {
|
|
262
284
|
const ends = new Map(segments.map((seg) => [seg.projectId, latest(seg.taskIds.map((id) => dates.finish.get(id)), seg.start)]));
|
|
263
285
|
return (id) => ends.get(segmentOf.get(id).projectId);
|
|
@@ -303,6 +325,34 @@ function involving(overallocations, seg) {
|
|
|
303
325
|
const ids = new Set(seg.taskIds);
|
|
304
326
|
return overallocations.filter((o) => o.taskIds.some((id) => ids.has(id)));
|
|
305
327
|
}
|
|
328
|
+
/** `date` moved by `hours` working hours on `calendar` (back when negative). */
|
|
329
|
+
function shiftWorking(date, hours, calendar) {
|
|
330
|
+
if (hours > 0)
|
|
331
|
+
return addWorkingHours(date, hours, calendar);
|
|
332
|
+
if (hours < 0)
|
|
333
|
+
return subtractWorkingHours(date, -hours, calendar);
|
|
334
|
+
return date;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* When a task may start, given when its predecessors finish: the latest of its project's start, its
|
|
338
|
+
* start constraint and each predecessor's finish plus the lag on that dependency.
|
|
339
|
+
*/
|
|
340
|
+
function readyTime(id, prerequisites, finish, ctx) {
|
|
341
|
+
let ready = ctx.startOf(id);
|
|
342
|
+
const floor = ctx.notBefore.get(id);
|
|
343
|
+
if (floor && floor > ready)
|
|
344
|
+
ready = floor;
|
|
345
|
+
const calendar = ctx.calendarOf.get(id);
|
|
346
|
+
for (const p of prerequisites.get(id)) {
|
|
347
|
+
const f = finish.get(p);
|
|
348
|
+
if (!f)
|
|
349
|
+
continue;
|
|
350
|
+
const after = shiftWorking(f, ctx.lagOf(p, id), calendar);
|
|
351
|
+
if (after > ready)
|
|
352
|
+
ready = after;
|
|
353
|
+
}
|
|
354
|
+
return ready;
|
|
355
|
+
}
|
|
306
356
|
function latest(dates, floor) {
|
|
307
357
|
let max = floor;
|
|
308
358
|
for (const d of dates)
|
|
@@ -331,12 +381,7 @@ function forwardPass(order, prerequisites, ctx) {
|
|
|
331
381
|
const start = new Map();
|
|
332
382
|
const finish = new Map();
|
|
333
383
|
for (const id of order) {
|
|
334
|
-
|
|
335
|
-
for (const p of prerequisites.get(id)) {
|
|
336
|
-
const f = finish.get(p);
|
|
337
|
-
if (f && f > ready)
|
|
338
|
-
ready = f;
|
|
339
|
-
}
|
|
384
|
+
const ready = readyTime(id, prerequisites, finish, ctx);
|
|
340
385
|
const calendar = ctx.calendarOf.get(id);
|
|
341
386
|
const duration = ctx.durationOf.get(id);
|
|
342
387
|
const es = startTime(ready, duration, calendar);
|
|
@@ -353,8 +398,10 @@ function backwardPass(order, successorsOf, endOf, ctx) {
|
|
|
353
398
|
let due = endOf(id);
|
|
354
399
|
for (const s of successorsOf(id)) {
|
|
355
400
|
const ls = start.get(s);
|
|
356
|
-
|
|
357
|
-
|
|
401
|
+
// The successor's late start, less the lag it waits after this task finishes
|
|
402
|
+
const by = ls && shiftWorking(ls, -ctx.lagOf(id, s), ctx.calendarOf.get(s));
|
|
403
|
+
if (by && by < due)
|
|
404
|
+
due = by;
|
|
358
405
|
}
|
|
359
406
|
const deadline = ctx.deadlineOf.get(id);
|
|
360
407
|
if (deadline && deadline < due)
|
|
@@ -424,12 +471,7 @@ function levelSchedule({ taskMap, taskIds, prerequisites, successors, topoOrder
|
|
|
424
471
|
eligible.add(topoOrder.find((id) => !start.has(id)));
|
|
425
472
|
const id = first(eligible);
|
|
426
473
|
eligible.delete(id);
|
|
427
|
-
|
|
428
|
-
for (const p of prerequisites.get(id)) {
|
|
429
|
-
const f = finish.get(p);
|
|
430
|
-
if (f && f > ready)
|
|
431
|
-
ready = f;
|
|
432
|
-
}
|
|
474
|
+
const ready = readyTime(id, prerequisites, finish, ctx);
|
|
433
475
|
const calendar = ctx.calendarOf.get(id);
|
|
434
476
|
const duration = ctx.durationOf.get(id);
|
|
435
477
|
const resources = duration > 0 ? resourcesOf(taskMap.get(id)) : [];
|
|
@@ -483,10 +525,36 @@ function levelSchedule({ taskMap, taskIds, prerequisites, successors, topoOrder
|
|
|
483
525
|
// whose finish freed its capacity.
|
|
484
526
|
for (const [taskId, blocker] of waitingOn)
|
|
485
527
|
link(blocker, taskId);
|
|
486
|
-
//
|
|
487
|
-
//
|
|
528
|
+
// The backward pass needs every successor (dependency or resource chain) before its
|
|
529
|
+
// predecessors. Edges mostly go forward in levelled start time, but a lead (negative lag) can
|
|
530
|
+
// start a successor first, so walk the edges depth-first, visiting tasks in start order.
|
|
488
531
|
const placement = new Map(placed.map((id, i) => [id, i]));
|
|
489
|
-
const
|
|
532
|
+
const byStart = [...taskIds].sort((a, b) => start.get(a).getTime() - start.get(b).getTime() || placement.get(a) - placement.get(b));
|
|
533
|
+
const backwardOrder = [];
|
|
534
|
+
const visited = new Set();
|
|
535
|
+
const visit = (root) => {
|
|
536
|
+
// Iterative post-order, so long chains cannot overflow the stack
|
|
537
|
+
const stack = [];
|
|
538
|
+
const enter = (id) => {
|
|
539
|
+
visited.add(id);
|
|
540
|
+
stack.push([id, [...successors.get(id), ...(resourceSuccessors.get(id) ?? [])][Symbol.iterator]()]);
|
|
541
|
+
};
|
|
542
|
+
enter(root);
|
|
543
|
+
while (stack.length) {
|
|
544
|
+
const [id, next] = stack[stack.length - 1];
|
|
545
|
+
const step = next.next();
|
|
546
|
+
if (step.done) {
|
|
547
|
+
stack.pop();
|
|
548
|
+
backwardOrder.push(id);
|
|
549
|
+
}
|
|
550
|
+
else if (!visited.has(step.value)) {
|
|
551
|
+
enter(step.value);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
for (const id of byStart)
|
|
556
|
+
if (!visited.has(id))
|
|
557
|
+
visit(id);
|
|
490
558
|
return {
|
|
491
559
|
early: { start, finish },
|
|
492
560
|
waitingOn,
|
|
@@ -698,145 +766,30 @@ export function calculateCPM(projectId, tasks, dependencies, options = {}) {
|
|
|
698
766
|
tasks: []
|
|
699
767
|
};
|
|
700
768
|
}
|
|
701
|
-
const graph = buildGraph(tasks, dependencies);
|
|
702
|
-
const { taskMap, taskIds, prerequisites, successors, topoOrder } = graph;
|
|
703
769
|
if (options.levelResources && options.calendars !== 'assignee') {
|
|
704
770
|
throw new Error("levelResources requires calendars: 'assignee'.");
|
|
705
771
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
// Deadlines as project-calendar hours from the start. Without a start date there is nothing to
|
|
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);
|
|
717
|
-
if (projectStartDate) {
|
|
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));
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
// --- FORWARD PASS ---
|
|
728
|
-
const earlyStart = new Map();
|
|
729
|
-
const earlyFinish = new Map();
|
|
730
|
-
for (const id of topoOrder) {
|
|
731
|
-
const task = taskMap.get(id);
|
|
732
|
-
const duration = getTaskScheduledHours(task, options.effort, options.defaultEstimateHours);
|
|
733
|
-
const prereqs = prerequisites.get(id) || new Set();
|
|
734
|
-
let maxPrereqFinish = 0;
|
|
735
|
-
for (const p of prereqs) {
|
|
736
|
-
const pFinish = earlyFinish.get(p) ?? 0;
|
|
737
|
-
if (pFinish > maxPrereqFinish) {
|
|
738
|
-
maxPrereqFinish = pFinish;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
const es = maxPrereqFinish;
|
|
742
|
-
const ef = es + duration;
|
|
743
|
-
earlyStart.set(id, es);
|
|
744
|
-
earlyFinish.set(id, ef);
|
|
745
|
-
}
|
|
746
|
-
// Total project duration
|
|
747
|
-
let totalDurationHours = 0;
|
|
748
|
-
for (const ef of earlyFinish.values()) {
|
|
749
|
-
if (ef > totalDurationHours) {
|
|
750
|
-
totalDurationHours = ef;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
// --- BACKWARD PASS ---
|
|
754
|
-
const lateStart = new Map();
|
|
755
|
-
const lateFinish = new Map();
|
|
756
|
-
// Process in reverse topological order
|
|
757
|
-
for (let i = topoOrder.length - 1; i >= 0; i--) {
|
|
758
|
-
const id = topoOrder[i];
|
|
759
|
-
const task = taskMap.get(id);
|
|
760
|
-
const duration = getTaskScheduledHours(task, options.effort, options.defaultEstimateHours);
|
|
761
|
-
const succs = successors.get(id) || new Set();
|
|
762
|
-
let minSuccLateStart = totalDurationHours;
|
|
763
|
-
if (succs.size > 0) {
|
|
764
|
-
minSuccLateStart = Infinity;
|
|
765
|
-
for (const s of succs) {
|
|
766
|
-
const sLateStart = lateStart.get(s);
|
|
767
|
-
if (sLateStart !== undefined && sLateStart < minSuccLateStart) {
|
|
768
|
-
minSuccLateStart = sLateStart;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (!Number.isFinite(minSuccLateStart)) {
|
|
772
|
-
minSuccLateStart = totalDurationHours;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
const lf = Math.min(minSuccLateStart, capHours.get(id) ?? Infinity);
|
|
776
|
-
const ls = lf - duration;
|
|
777
|
-
lateFinish.set(id, lf);
|
|
778
|
-
lateStart.set(id, ls);
|
|
772
|
+
// Both modes run the same date-based passes; project mode puts every task on the project calendar.
|
|
773
|
+
if (options.calendars === 'assignee' || projectStartDate) {
|
|
774
|
+
return calculateCalendarCPM(projectId, calculatedAt, buildGraph(tasks, dependencies), options);
|
|
779
775
|
}
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
: undefined;
|
|
799
|
-
const earlyFinishDate = projectStartDate
|
|
800
|
-
? addWorkingHours(projectStartDate, ef, schedule).toISOString()
|
|
801
|
-
: undefined;
|
|
802
|
-
const lateStartDate = projectStartDate
|
|
803
|
-
? addWorkingHours(projectStartDate, ls, schedule).toISOString()
|
|
804
|
-
: undefined;
|
|
805
|
-
const lateFinishDate = projectStartDate
|
|
806
|
-
? addWorkingHours(projectStartDate, lf, schedule).toISOString()
|
|
807
|
-
: undefined;
|
|
808
|
-
schedules.push({
|
|
809
|
-
taskId: id,
|
|
810
|
-
earlyStart: Math.round(es * 100) / 100,
|
|
811
|
-
earlyFinish: Math.round(ef * 100) / 100,
|
|
812
|
-
lateStart: Math.round(ls * 100) / 100,
|
|
813
|
-
lateFinish: Math.round(lf * 100) / 100,
|
|
814
|
-
totalSlack,
|
|
815
|
-
isCritical,
|
|
816
|
-
durationHours: duration,
|
|
817
|
-
...allocationField(task),
|
|
818
|
-
...milestoneFields(task, ownDeadline.get(id), ownDeadline.has(id) ? toHours(ownDeadline.get(id)) - ef : undefined),
|
|
819
|
-
slackWorkingHours: totalSlack,
|
|
820
|
-
earlyStartDate,
|
|
821
|
-
earlyFinishDate,
|
|
822
|
-
lateStartDate,
|
|
823
|
-
lateFinishDate
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
const projectEndDate = projectStartDate
|
|
827
|
-
? addWorkingHours(projectStartDate, totalDurationHours, schedule).toISOString()
|
|
828
|
-
: undefined;
|
|
829
|
-
return finishAnalysis({
|
|
830
|
-
projectId,
|
|
831
|
-
calculatedAt,
|
|
832
|
-
totalDurationHours: Math.round(totalDurationHours * 100) / 100,
|
|
833
|
-
totalWorkingHours: Math.round(totalDurationHours * 100) / 100,
|
|
834
|
-
projectStartDate: projectStartDate ? projectStartDate.toISOString() : undefined,
|
|
835
|
-
projectEndDate,
|
|
836
|
-
criticalTaskIds,
|
|
837
|
-
missedDeadlineTaskIds: missedDeadlines(schedules),
|
|
838
|
-
...(graph.cyclicTaskIds.length ? { cyclicTaskIds: graph.cyclicTaskIds } : {}),
|
|
839
|
-
tasks: schedules
|
|
840
|
-
}, graph, schedule, taskIds);
|
|
776
|
+
// Without a start date there is nothing to date the schedule from. It runs from a fixed anchor and
|
|
777
|
+
// reports working-hour offsets only, which do not depend on the anchor because they count working
|
|
778
|
+
// time on the one calendar. Deadlines and start constraints are dates, so they do not apply.
|
|
779
|
+
const undatedTasks = tasks.map(({ deadline: _deadline, earliestStart: _earliestStart, ...task }) => task);
|
|
780
|
+
const analysis = calculateCalendarCPM(projectId, calculatedAt, buildGraph(undatedTasks, dependencies), {
|
|
781
|
+
...options,
|
|
782
|
+
projectStartDate: UNDATED_ANCHOR
|
|
783
|
+
});
|
|
784
|
+
return withoutDates(analysis);
|
|
785
|
+
}
|
|
786
|
+
/** A Monday at UTC midnight: where an undated project-calendar schedule is laid out. */
|
|
787
|
+
const UNDATED_ANCHOR = '2001-01-01T00:00:00.000Z';
|
|
788
|
+
/** Drops the dates from an analysis laid out from `UNDATED_ANCHOR`, keeping working-hour offsets. */
|
|
789
|
+
function withoutDates({ projectStartDate: _start, projectEndDate: _end, overallocations: _overallocations, ...analysis }) {
|
|
790
|
+
return {
|
|
791
|
+
...analysis,
|
|
792
|
+
tasks: analysis.tasks.map(({ earlyStartDate: _es, earlyFinishDate: _ef, lateStartDate: _ls, lateFinishDate: _lf, ...task }) => task)
|
|
793
|
+
};
|
|
841
794
|
}
|
|
842
795
|
//# sourceMappingURL=cpm.js.map
|