@diagrammo/dgmo 0.47.0 → 0.48.1
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/advanced.js +11 -11
- package/dist/auto.js +16 -16
- package/dist/auto.mjs +41 -12
- package/dist/block.js +4 -4
- package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
- package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
- package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
- package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
- package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
- package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
- package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
- package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
- package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
- package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
- package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
- package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
- package/dist/cli.cjs +15 -15
- package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
- package/dist/element.js +16 -16
- package/dist/element.mjs +41 -12
- package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -4
- package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
- package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
- package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
- package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
- package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
- package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
- package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
- package/package.json +23 -71
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
package/src/gantt/calculator.ts
DELETED
|
@@ -1,979 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Gantt Schedule Calculator
|
|
3
|
-
// ============================================================
|
|
4
|
-
//
|
|
5
|
-
// Takes a ParsedGantt and resolves all tasks to concrete start/end dates.
|
|
6
|
-
// Algorithm:
|
|
7
|
-
// 1. Flatten task tree, build dependency graph
|
|
8
|
-
// 2. Topological sort with cycle detection
|
|
9
|
-
// 3. Forward pass: resolve dates using universal max rule
|
|
10
|
-
// 4. (Optional) Critical path: backward pass to find zero-slack chain
|
|
11
|
-
|
|
12
|
-
import { makeDgmoError } from '../diagnostics';
|
|
13
|
-
import type { DgmoError } from '../diagnostics';
|
|
14
|
-
import type {
|
|
15
|
-
ParsedGantt,
|
|
16
|
-
GanttNode,
|
|
17
|
-
GanttTask,
|
|
18
|
-
GanttGroup,
|
|
19
|
-
GanttHolidays,
|
|
20
|
-
GanttOptions,
|
|
21
|
-
Duration,
|
|
22
|
-
ResolvedSchedule,
|
|
23
|
-
ResolvedSprint,
|
|
24
|
-
ResolvedGroup,
|
|
25
|
-
Offset,
|
|
26
|
-
} from './types';
|
|
27
|
-
import { collectTasks, resolveTaskName, isResolverError } from './resolver';
|
|
28
|
-
import {
|
|
29
|
-
addGanttDuration,
|
|
30
|
-
buildHolidaySet,
|
|
31
|
-
parseGanttDate,
|
|
32
|
-
} from '../utils/duration';
|
|
33
|
-
|
|
34
|
-
// ── Internal types ──────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
interface TaskNode {
|
|
37
|
-
task: GanttTask;
|
|
38
|
-
/** IDs of tasks this task depends on (both implicit and explicit) */
|
|
39
|
-
predecessors: string[];
|
|
40
|
-
/** Resolved start/end dates (filled during forward pass) */
|
|
41
|
-
startDate: Date | null;
|
|
42
|
-
endDate: Date | null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ── Main calculator ─────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
|
|
48
|
-
const diagnostics: DgmoError[] = [...parsed.diagnostics];
|
|
49
|
-
const result: ResolvedSchedule = {
|
|
50
|
-
tasks: [],
|
|
51
|
-
groups: [],
|
|
52
|
-
startDate: new Date(),
|
|
53
|
-
endDate: new Date(),
|
|
54
|
-
holidays: parsed.holidays,
|
|
55
|
-
tagGroups: parsed.tagGroups.slice(),
|
|
56
|
-
eras: [],
|
|
57
|
-
markers: [],
|
|
58
|
-
sprints: [],
|
|
59
|
-
options: parsed.options,
|
|
60
|
-
diagnostics,
|
|
61
|
-
error: parsed.error,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
if (parsed.error) return result;
|
|
65
|
-
|
|
66
|
-
const warn = (line: number, message: string): void => {
|
|
67
|
-
diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// ── Build holiday set ───────────────────────────────────
|
|
71
|
-
|
|
72
|
-
const holidaySet = buildHolidaySet(parsed.holidays);
|
|
73
|
-
|
|
74
|
-
// ── Determine project start ─────────────────────────────
|
|
75
|
-
|
|
76
|
-
let projectStart: Date;
|
|
77
|
-
if (parsed.options.start) {
|
|
78
|
-
projectStart = parseGanttDate(parsed.options.start);
|
|
79
|
-
} else {
|
|
80
|
-
// Relative timeline: use epoch-like reference (Day 1 = Jan 1, 2000)
|
|
81
|
-
projectStart = new Date(2000, 0, 1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ── Resolve offset-based eras and markers to absolute dates ──
|
|
85
|
-
|
|
86
|
-
result.eras = parsed.eras.map((era) => {
|
|
87
|
-
if (era.offsetStart && era.offsetEnd) {
|
|
88
|
-
const start = addGanttDuration(
|
|
89
|
-
projectStart,
|
|
90
|
-
era.offsetStart.duration,
|
|
91
|
-
parsed.holidays,
|
|
92
|
-
new Set(),
|
|
93
|
-
era.offsetStart.direction
|
|
94
|
-
);
|
|
95
|
-
const end = addGanttDuration(
|
|
96
|
-
projectStart,
|
|
97
|
-
era.offsetEnd.duration,
|
|
98
|
-
parsed.holidays,
|
|
99
|
-
new Set(),
|
|
100
|
-
era.offsetEnd.direction
|
|
101
|
-
);
|
|
102
|
-
return { ...era, startDate: formatDate(start), endDate: formatDate(end) };
|
|
103
|
-
}
|
|
104
|
-
return era;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
result.markers = parsed.markers.map((m) => {
|
|
108
|
-
if (m.offsetDate) {
|
|
109
|
-
const d = addGanttDuration(
|
|
110
|
-
projectStart,
|
|
111
|
-
m.offsetDate.duration,
|
|
112
|
-
parsed.holidays,
|
|
113
|
-
new Set(),
|
|
114
|
-
m.offsetDate.direction
|
|
115
|
-
);
|
|
116
|
-
return { ...m, date: formatDate(d) };
|
|
117
|
-
}
|
|
118
|
-
return m;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// ── Sprint config ──────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
const sprintOpts = parsed.options.sprintLength
|
|
124
|
-
? { sprintLength: parsed.options.sprintLength }
|
|
125
|
-
: undefined;
|
|
126
|
-
|
|
127
|
-
// ── Dep offset storage ─────────────────────────────────
|
|
128
|
-
|
|
129
|
-
const depOffsetMap = new Map<string, Offset>();
|
|
130
|
-
|
|
131
|
-
// ── Collect all tasks ───────────────────────────────────
|
|
132
|
-
|
|
133
|
-
const allTasks = collectTasks(parsed.nodes);
|
|
134
|
-
if (allTasks.length === 0) {
|
|
135
|
-
return result; // empty chart is valid
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Build task map by ID
|
|
139
|
-
const taskMap = new Map<string, TaskNode>();
|
|
140
|
-
for (const task of allTasks) {
|
|
141
|
-
taskMap.set(task.id, {
|
|
142
|
-
task,
|
|
143
|
-
predecessors: [],
|
|
144
|
-
startDate: null,
|
|
145
|
-
endDate: null,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── Build implicit sequential dependencies ──────────────
|
|
150
|
-
// New-mode charts have explicit graph structure (all deps from arrows).
|
|
151
|
-
// Legacy charts need implicit sequential deps between siblings.
|
|
152
|
-
|
|
153
|
-
if (parsed.syntaxMode === 'legacy') {
|
|
154
|
-
buildImplicitDeps(parsed.nodes, taskMap);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── Resolve explicit -> dependencies ────────────────────
|
|
158
|
-
|
|
159
|
-
for (const task of allTasks) {
|
|
160
|
-
for (const dep of task.dependencies) {
|
|
161
|
-
const resolved = resolveTaskName(dep.targetName, allTasks);
|
|
162
|
-
if (isResolverError(resolved)) {
|
|
163
|
-
warn(dep.lineNumber, `\`-> ${dep.targetName}\` — ${resolved.message}`);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// The dependency means: target starts after source
|
|
168
|
-
// In our model: target's predecessor is source
|
|
169
|
-
const targetNode = taskMap.get(resolved.task.id);
|
|
170
|
-
if (targetNode) {
|
|
171
|
-
// Check for redundant dependency (already a sequential predecessor)
|
|
172
|
-
if (targetNode.predecessors.includes(task.id)) {
|
|
173
|
-
warn(
|
|
174
|
-
dep.lineNumber,
|
|
175
|
-
parsed.syntaxMode === 'new'
|
|
176
|
-
? `Redundant dependency: "${dep.targetName}" already follows "${task.label}".`
|
|
177
|
-
: `Redundant dependency: "${dep.targetName}" already follows "${task.label}" sequentially. Did you mean to wrap groups in \`parallel\`?`
|
|
178
|
-
);
|
|
179
|
-
} else {
|
|
180
|
-
targetNode.predecessors.push(task.id);
|
|
181
|
-
// Store dep offset info — we need it during scheduling
|
|
182
|
-
if (dep.offset) {
|
|
183
|
-
depOffsetMap.set(`${task.id}->${resolved.task.id}`, dep.offset);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ── Topological sort with cycle detection ───────────────
|
|
191
|
-
|
|
192
|
-
let sortedIds = topologicalSort(taskMap);
|
|
193
|
-
if (!sortedIds) {
|
|
194
|
-
// Find cycle, warn, and break it by removing one explicit dep edge
|
|
195
|
-
const cycle = findCycle(taskMap);
|
|
196
|
-
const cycleStr = cycle.map((id) => taskMap.get(id)!.task.label).join(' → ');
|
|
197
|
-
warn(
|
|
198
|
-
// Cycle is non-empty when topo sort failed; preserves prior crash-on-empty behavior.
|
|
199
|
-
taskMap.get(cycle[0]!)!.task.lineNumber,
|
|
200
|
-
`Circular dependency detected: ${cycleStr}. The cycle-creating dependency was dropped.`
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
// Remove the last edge in the cycle to break it
|
|
204
|
-
// (prefer removing explicit -> deps over implicit sequential ones)
|
|
205
|
-
breakCycle(cycle, taskMap, depOffsetMap);
|
|
206
|
-
|
|
207
|
-
// Retry — if still cyclic after breaking, keep breaking until resolved
|
|
208
|
-
sortedIds = topologicalSort(taskMap);
|
|
209
|
-
let safety = 10;
|
|
210
|
-
while (!sortedIds && safety-- > 0) {
|
|
211
|
-
const nextCycle = findCycle(taskMap);
|
|
212
|
-
if (nextCycle.length === 0) break;
|
|
213
|
-
breakCycle(nextCycle, taskMap, depOffsetMap);
|
|
214
|
-
sortedIds = topologicalSort(taskMap);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (!sortedIds) {
|
|
218
|
-
// Truly unresolvable — fall back to task insertion order
|
|
219
|
-
sortedIds = [...taskMap.keys()];
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ── Forward pass: resolve dates ─────────────────────────
|
|
224
|
-
|
|
225
|
-
for (const taskId of sortedIds) {
|
|
226
|
-
const node = taskMap.get(taskId)!;
|
|
227
|
-
const task = node.task;
|
|
228
|
-
|
|
229
|
-
// Determine start date: max of all predecessors' end dates (+ dep offset)
|
|
230
|
-
let start: Date;
|
|
231
|
-
|
|
232
|
-
if (task.explicitStart) {
|
|
233
|
-
// Explicit date anchor
|
|
234
|
-
start = parseGanttDate(task.explicitStart);
|
|
235
|
-
} else if (node.predecessors.length === 0) {
|
|
236
|
-
// No predecessors: starts at project start
|
|
237
|
-
start = new Date(projectStart);
|
|
238
|
-
} else {
|
|
239
|
-
// Universal max rule: start = max(all predecessor end dates + dep offset)
|
|
240
|
-
start = new Date(-8640000000000000);
|
|
241
|
-
for (const predId of node.predecessors) {
|
|
242
|
-
const predNode = taskMap.get(predId)!;
|
|
243
|
-
if (!predNode.endDate) continue; // shouldn't happen after topo sort
|
|
244
|
-
|
|
245
|
-
let predEnd = new Date(predNode.endDate);
|
|
246
|
-
|
|
247
|
-
// Apply dep offset if present
|
|
248
|
-
const depOffset = depOffsetMap.get(`${predId}->${taskId}`);
|
|
249
|
-
if (depOffset) {
|
|
250
|
-
predEnd = addGanttDuration(
|
|
251
|
-
predEnd,
|
|
252
|
-
depOffset.duration,
|
|
253
|
-
parsed.holidays,
|
|
254
|
-
holidaySet,
|
|
255
|
-
depOffset.direction,
|
|
256
|
-
sprintOpts
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (predEnd.getTime() > start.getTime()) {
|
|
261
|
-
start = predEnd;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Apply group offset as floor constraint (new mode only)
|
|
267
|
-
if (parsed.syntaxMode === 'new' && task.groupPath.length > 0) {
|
|
268
|
-
const groupOffset = findGroupOffset(parsed.nodes, task.groupPath);
|
|
269
|
-
if (groupOffset) {
|
|
270
|
-
const groupFloor = addGanttDuration(
|
|
271
|
-
new Date(projectStart),
|
|
272
|
-
groupOffset.duration,
|
|
273
|
-
parsed.holidays,
|
|
274
|
-
holidaySet,
|
|
275
|
-
groupOffset.direction,
|
|
276
|
-
sprintOpts
|
|
277
|
-
);
|
|
278
|
-
if (groupFloor.getTime() > start.getTime()) {
|
|
279
|
-
start = groupFloor;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Apply task-level offset (shifts start forward or backward)
|
|
285
|
-
if (task.offset) {
|
|
286
|
-
start = addGanttDuration(
|
|
287
|
-
start,
|
|
288
|
-
task.offset.duration,
|
|
289
|
-
parsed.holidays,
|
|
290
|
-
holidaySet,
|
|
291
|
-
task.offset.direction,
|
|
292
|
-
sprintOpts
|
|
293
|
-
);
|
|
294
|
-
if (start.getTime() < projectStart.getTime()) {
|
|
295
|
-
warn(
|
|
296
|
-
task.lineNumber,
|
|
297
|
-
`Negative offset on task '${task.label}' exceeds available range; start clamped to project start.`
|
|
298
|
-
);
|
|
299
|
-
start = new Date(projectStart);
|
|
300
|
-
}
|
|
301
|
-
} else if (start.getTime() < projectStart.getTime()) {
|
|
302
|
-
warn(
|
|
303
|
-
task.lineNumber,
|
|
304
|
-
`Negative offset on dependency exceeds available range; start of '${task.label}' clamped to project start.`
|
|
305
|
-
);
|
|
306
|
-
start = new Date(projectStart);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// If explicit start (+ offset) conflicts with predecessors, warn but honor explicit
|
|
310
|
-
if (task.explicitStart && node.predecessors.length > 0) {
|
|
311
|
-
let maxPredEnd = new Date(-8640000000000000);
|
|
312
|
-
for (const predId of node.predecessors) {
|
|
313
|
-
const predNode = taskMap.get(predId)!;
|
|
314
|
-
if (
|
|
315
|
-
predNode.endDate &&
|
|
316
|
-
predNode.endDate.getTime() > maxPredEnd.getTime()
|
|
317
|
-
) {
|
|
318
|
-
maxPredEnd = predNode.endDate;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (start.getTime() < maxPredEnd.getTime()) {
|
|
322
|
-
warn(
|
|
323
|
-
task.lineNumber,
|
|
324
|
-
`Explicit date ${task.explicitStart}${task.offset ? ' (with offset)' : ''} overlaps with predecessor ending ${formatDate(maxPredEnd)}. Using explicit date.`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Calculate end date
|
|
330
|
-
let end: Date;
|
|
331
|
-
if (task.duration) {
|
|
332
|
-
if (task.duration.amount === 0) {
|
|
333
|
-
// Milestone: zero duration, end = start
|
|
334
|
-
end = new Date(start);
|
|
335
|
-
} else {
|
|
336
|
-
end = addGanttDuration(
|
|
337
|
-
start,
|
|
338
|
-
task.duration,
|
|
339
|
-
parsed.holidays,
|
|
340
|
-
holidaySet,
|
|
341
|
-
1,
|
|
342
|
-
sprintOpts
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
} else {
|
|
346
|
-
// Explicit date task with no duration = milestone at that date
|
|
347
|
-
end = new Date(start);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
node.startDate = start;
|
|
351
|
-
node.endDate = end;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ── Build resolved tasks ────────────────────────────────
|
|
355
|
-
|
|
356
|
-
// Critical path calculation (if enabled)
|
|
357
|
-
const criticalSet = parsed.options.criticalPath
|
|
358
|
-
? computeCriticalPath(
|
|
359
|
-
sortedIds,
|
|
360
|
-
taskMap,
|
|
361
|
-
depOffsetMap,
|
|
362
|
-
parsed.holidays,
|
|
363
|
-
holidaySet,
|
|
364
|
-
sprintOpts
|
|
365
|
-
)
|
|
366
|
-
: new Set<string>();
|
|
367
|
-
|
|
368
|
-
// Cascading uncertainty: uncertain if task itself is uncertain OR any predecessor is
|
|
369
|
-
const uncertainSet = new Set<string>();
|
|
370
|
-
for (const taskId of sortedIds) {
|
|
371
|
-
const node = taskMap.get(taskId)!;
|
|
372
|
-
if (node.task.uncertain) {
|
|
373
|
-
uncertainSet.add(taskId);
|
|
374
|
-
} else {
|
|
375
|
-
for (const predId of node.predecessors) {
|
|
376
|
-
if (uncertainSet.has(predId)) {
|
|
377
|
-
uncertainSet.add(taskId);
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
for (const taskId of sortedIds) {
|
|
385
|
-
const node = taskMap.get(taskId)!;
|
|
386
|
-
result.tasks.push({
|
|
387
|
-
task: node.task,
|
|
388
|
-
startDate: node.startDate!,
|
|
389
|
-
endDate: node.endDate!,
|
|
390
|
-
isCriticalPath: criticalSet.has(taskId),
|
|
391
|
-
isUncertain: uncertainSet.has(taskId),
|
|
392
|
-
isMilestone:
|
|
393
|
-
node.task.duration?.amount === 0 ||
|
|
394
|
-
(!node.task.duration && !node.task.explicitStart),
|
|
395
|
-
groupPath: node.task.groupPath.slice(),
|
|
396
|
-
effectiveMetadata: { ...node.task.metadata },
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// ── Build resolved groups ───────────────────────────────
|
|
401
|
-
|
|
402
|
-
buildResolvedGroups(parsed.nodes, taskMap, result.groups, 0);
|
|
403
|
-
|
|
404
|
-
// ── Compute overall date range ──────────────────────────
|
|
405
|
-
|
|
406
|
-
if (result.tasks.length > 0) {
|
|
407
|
-
// In-bounds by result.tasks.length > 0 guard.
|
|
408
|
-
let minDate = result.tasks[0]!.startDate;
|
|
409
|
-
let maxDate = result.tasks[0]!.endDate;
|
|
410
|
-
for (const t of result.tasks) {
|
|
411
|
-
if (t.startDate.getTime() < minDate.getTime()) minDate = t.startDate;
|
|
412
|
-
if (t.endDate.getTime() > maxDate.getTime()) maxDate = t.endDate;
|
|
413
|
-
}
|
|
414
|
-
result.startDate = minDate;
|
|
415
|
-
result.endDate = maxDate;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ── Generate sprint bands ──────────────────────────────
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
parsed.options.sprintMode &&
|
|
422
|
-
parsed.options.sprintLength &&
|
|
423
|
-
result.tasks.length > 0
|
|
424
|
-
) {
|
|
425
|
-
result.sprints = generateSprintBands(
|
|
426
|
-
parsed.options,
|
|
427
|
-
result.startDate,
|
|
428
|
-
result.endDate,
|
|
429
|
-
projectStart
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
// Extend chart range to include the last sprint's end so it doesn't clip
|
|
433
|
-
if (result.sprints.length > 0) {
|
|
434
|
-
// In-bounds by result.sprints.length > 0 guard.
|
|
435
|
-
const lastSprintEnd = result.sprints[result.sprints.length - 1]!.endDate;
|
|
436
|
-
if (lastSprintEnd.getTime() > result.endDate.getTime()) {
|
|
437
|
-
result.endDate = lastSprintEnd;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ── Warnings ────────────────────────────────────────────
|
|
443
|
-
|
|
444
|
-
// Missing parallel warning: 2+ top-level groups without parallel wrapper (legacy only)
|
|
445
|
-
if (parsed.syntaxMode === 'legacy') {
|
|
446
|
-
const topLevelGroups = parsed.nodes.filter((n) => n.kind === 'group');
|
|
447
|
-
if (topLevelGroups.length >= 2) {
|
|
448
|
-
const names = topLevelGroups.map(
|
|
449
|
-
(g) => (g as GanttGroup & { kind: 'group' }).name
|
|
450
|
-
);
|
|
451
|
-
warn(
|
|
452
|
-
topLevelGroups[0]!.lineNumber,
|
|
453
|
-
`${names.join(' and ')} are sequential. Wrap in \`parallel\` if they should run concurrently.`
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return result;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ── Implicit dependency builder ─────────────────────────────
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Walk the gantt tree and create implicit sequential dependencies.
|
|
465
|
-
* - Siblings are sequential (each task depends on the previous sibling)
|
|
466
|
-
* - Parallel block children all share the parent's predecessor
|
|
467
|
-
* - Groups: children are sequential within the group
|
|
468
|
-
*/
|
|
469
|
-
function buildImplicitDeps(
|
|
470
|
-
nodes: readonly GanttNode[],
|
|
471
|
-
taskMap: Map<string, TaskNode>
|
|
472
|
-
): void {
|
|
473
|
-
walkChildren(nodes, null);
|
|
474
|
-
|
|
475
|
-
function walkChildren(
|
|
476
|
-
children: readonly GanttNode[],
|
|
477
|
-
afterTaskId: string | null
|
|
478
|
-
) {
|
|
479
|
-
let prevTaskId = afterTaskId;
|
|
480
|
-
|
|
481
|
-
for (const node of children) {
|
|
482
|
-
if (node.kind === 'task') {
|
|
483
|
-
if (prevTaskId) {
|
|
484
|
-
const taskNode = taskMap.get(node.id);
|
|
485
|
-
if (taskNode && !taskNode.predecessors.includes(prevTaskId)) {
|
|
486
|
-
taskNode.predecessors.push(prevTaskId);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
prevTaskId = node.id;
|
|
490
|
-
} else if (node.kind === 'group') {
|
|
491
|
-
// Walk group children sequentially, starting after prevTaskId
|
|
492
|
-
const lastId = walkSequential(node.children, prevTaskId);
|
|
493
|
-
if (lastId) prevTaskId = lastId;
|
|
494
|
-
} else if (node.kind === 'parallel') {
|
|
495
|
-
// All parallel children start after prevTaskId
|
|
496
|
-
// The parallel block "ends" when all children end
|
|
497
|
-
// We need to find the last task in each branch
|
|
498
|
-
const branchLastIds: string[] = [];
|
|
499
|
-
for (const child of node.children) {
|
|
500
|
-
if (child.kind === 'task') {
|
|
501
|
-
if (prevTaskId) {
|
|
502
|
-
const taskNode = taskMap.get(child.id);
|
|
503
|
-
if (taskNode && !taskNode.predecessors.includes(prevTaskId)) {
|
|
504
|
-
taskNode.predecessors.push(prevTaskId);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
branchLastIds.push(child.id);
|
|
508
|
-
} else if (child.kind === 'group') {
|
|
509
|
-
// First task in group depends on prevTaskId
|
|
510
|
-
const firstId = findFirstTask(child.children);
|
|
511
|
-
if (firstId && prevTaskId) {
|
|
512
|
-
const taskNode = taskMap.get(firstId);
|
|
513
|
-
if (taskNode && !taskNode.predecessors.includes(prevTaskId)) {
|
|
514
|
-
taskNode.predecessors.push(prevTaskId);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
// Walk sequentially within the group
|
|
518
|
-
walkSequential(child.children, null); // internal deps only
|
|
519
|
-
const lastId = findLastTask(child.children);
|
|
520
|
-
if (lastId) branchLastIds.push(lastId);
|
|
521
|
-
} else if (child.kind === 'parallel') {
|
|
522
|
-
// Nested parallel — recurse
|
|
523
|
-
walkChildren([child], prevTaskId);
|
|
524
|
-
const lastId = findLastTask(child.children);
|
|
525
|
-
if (lastId) branchLastIds.push(lastId);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
// After parallel, next sibling depends on ALL branch ends
|
|
529
|
-
if (branchLastIds.length > 0) {
|
|
530
|
-
prevTaskId = null;
|
|
531
|
-
const nextIdx = children.indexOf(node) + 1;
|
|
532
|
-
if (nextIdx < children.length) {
|
|
533
|
-
// In-bounds by nextIdx < children.length guard.
|
|
534
|
-
const nextFirstId = findFirstTask([children[nextIdx]!]);
|
|
535
|
-
if (nextFirstId) {
|
|
536
|
-
const nextNode = taskMap.get(nextFirstId);
|
|
537
|
-
if (nextNode) {
|
|
538
|
-
for (const branchId of branchLastIds) {
|
|
539
|
-
if (!nextNode.predecessors.includes(branchId)) {
|
|
540
|
-
nextNode.predecessors.push(branchId);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function walkSequential(
|
|
552
|
-
children: readonly GanttNode[],
|
|
553
|
-
afterTaskId: string | null
|
|
554
|
-
): string | null {
|
|
555
|
-
let prevTaskId = afterTaskId;
|
|
556
|
-
for (const node of children) {
|
|
557
|
-
if (node.kind === 'task') {
|
|
558
|
-
if (prevTaskId) {
|
|
559
|
-
const taskNode = taskMap.get(node.id);
|
|
560
|
-
if (taskNode && !taskNode.predecessors.includes(prevTaskId)) {
|
|
561
|
-
taskNode.predecessors.push(prevTaskId);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
prevTaskId = node.id;
|
|
565
|
-
} else if (node.kind === 'group') {
|
|
566
|
-
const lastId = walkSequential(node.children, prevTaskId);
|
|
567
|
-
if (lastId) prevTaskId = lastId;
|
|
568
|
-
} else if (node.kind === 'parallel') {
|
|
569
|
-
walkChildren([node], prevTaskId);
|
|
570
|
-
const lastId = findLastTask(node.children);
|
|
571
|
-
if (lastId) prevTaskId = lastId;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return prevTaskId;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/** Find the first task ID in a tree (depth-first). */
|
|
579
|
-
function findFirstTask(nodes: readonly GanttNode[]): string | null {
|
|
580
|
-
for (const node of nodes) {
|
|
581
|
-
if (node.kind === 'task') return node.id;
|
|
582
|
-
if (node.kind === 'group' || node.kind === 'parallel') {
|
|
583
|
-
const found = findFirstTask(node.children);
|
|
584
|
-
if (found) return found;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Find the last task ID in a tree (depth-first, last branch). */
|
|
591
|
-
function findLastTask(nodes: readonly GanttNode[]): string | null {
|
|
592
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
593
|
-
// In-bounds by reverse loop guard (i ranges over valid indices).
|
|
594
|
-
const node = nodes[i]!;
|
|
595
|
-
if (node.kind === 'task') return node.id;
|
|
596
|
-
if (node.kind === 'group' || node.kind === 'parallel') {
|
|
597
|
-
const found = findLastTask(node.children);
|
|
598
|
-
if (found) return found;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// ── Topological sort ────────────────────────────────────────
|
|
605
|
-
|
|
606
|
-
function topologicalSort(taskMap: Map<string, TaskNode>): string[] | null {
|
|
607
|
-
const inDegree = new Map<string, number>();
|
|
608
|
-
const adjacency = new Map<string, string[]>();
|
|
609
|
-
|
|
610
|
-
for (const [id, node] of taskMap) {
|
|
611
|
-
inDegree.set(id, node.predecessors.length);
|
|
612
|
-
for (const pred of node.predecessors) {
|
|
613
|
-
const adj = adjacency.get(pred) ?? [];
|
|
614
|
-
adj.push(id);
|
|
615
|
-
adjacency.set(pred, adj);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const queue: string[] = [];
|
|
620
|
-
for (const [id, degree] of inDegree) {
|
|
621
|
-
if (degree === 0) queue.push(id);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const sorted: string[] = [];
|
|
625
|
-
while (queue.length > 0) {
|
|
626
|
-
const current = queue.shift()!;
|
|
627
|
-
sorted.push(current);
|
|
628
|
-
for (const succ of adjacency.get(current) ?? []) {
|
|
629
|
-
const newDegree = (inDegree.get(succ) ?? 1) - 1;
|
|
630
|
-
inDegree.set(succ, newDegree);
|
|
631
|
-
if (newDegree === 0) queue.push(succ);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return sorted.length === taskMap.size ? sorted : null;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function findCycle(taskMap: Map<string, TaskNode>): string[] {
|
|
639
|
-
const visited = new Set<string>();
|
|
640
|
-
const onStack = new Set<string>();
|
|
641
|
-
const parent = new Map<string, string>();
|
|
642
|
-
|
|
643
|
-
for (const id of taskMap.keys()) {
|
|
644
|
-
if (!visited.has(id)) {
|
|
645
|
-
const cycle = dfs(id);
|
|
646
|
-
if (cycle) return cycle;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return [];
|
|
650
|
-
|
|
651
|
-
function dfs(id: string): string[] | null {
|
|
652
|
-
visited.add(id);
|
|
653
|
-
onStack.add(id);
|
|
654
|
-
|
|
655
|
-
const successors: string[] = [];
|
|
656
|
-
for (const [otherId, otherNode] of taskMap) {
|
|
657
|
-
if (otherNode.predecessors.includes(id)) {
|
|
658
|
-
successors.push(otherId);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
for (const succ of successors) {
|
|
663
|
-
if (!visited.has(succ)) {
|
|
664
|
-
parent.set(succ, id);
|
|
665
|
-
const cycle = dfs(succ);
|
|
666
|
-
if (cycle) return cycle;
|
|
667
|
-
} else if (onStack.has(succ)) {
|
|
668
|
-
const cycle = [succ];
|
|
669
|
-
let current = id;
|
|
670
|
-
while (current !== succ) {
|
|
671
|
-
cycle.push(current);
|
|
672
|
-
current = parent.get(current)!;
|
|
673
|
-
}
|
|
674
|
-
cycle.push(succ);
|
|
675
|
-
return cycle.reverse();
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
onStack.delete(id);
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Break a cycle by removing the last edge. The cycle array is [A, B, ..., A]
|
|
686
|
-
* (starts and ends with the same node). We remove A's predecessor edge to
|
|
687
|
-
* the penultimate node, which breaks the cycle.
|
|
688
|
-
*/
|
|
689
|
-
function breakCycle(
|
|
690
|
-
cycle: string[],
|
|
691
|
-
taskMap: Map<string, TaskNode>,
|
|
692
|
-
depOffsetMap: Map<string, Offset>
|
|
693
|
-
): void {
|
|
694
|
-
if (cycle.length < 3) return; // need at least [A, B, A]
|
|
695
|
-
// Remove the edge from second-to-last → first (i.e. the edge that closes the cycle)
|
|
696
|
-
// In-bounds by cycle.length >= 3 guard above.
|
|
697
|
-
const fromId = cycle[cycle.length - 2]!;
|
|
698
|
-
const toId = cycle[0]!;
|
|
699
|
-
const toNode = taskMap.get(toId);
|
|
700
|
-
if (toNode) {
|
|
701
|
-
const idx = toNode.predecessors.indexOf(fromId);
|
|
702
|
-
if (idx !== -1) {
|
|
703
|
-
toNode.predecessors.splice(idx, 1);
|
|
704
|
-
depOffsetMap.delete(`${fromId}->${toId}`);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// ── Critical path ───────────────────────────────────────────
|
|
710
|
-
|
|
711
|
-
function computeCriticalPath(
|
|
712
|
-
sortedIds: string[],
|
|
713
|
-
taskMap: Map<string, TaskNode>,
|
|
714
|
-
depOffsetMap: Map<string, Offset>,
|
|
715
|
-
holidays: GanttHolidays,
|
|
716
|
-
holidaySet: Set<string>,
|
|
717
|
-
sprintOpts?: { sprintLength?: Duration }
|
|
718
|
-
): Set<string> {
|
|
719
|
-
if (sortedIds.length === 0) return new Set();
|
|
720
|
-
|
|
721
|
-
const latestEnd = new Map<string, number>();
|
|
722
|
-
const latestStart = new Map<string, number>();
|
|
723
|
-
|
|
724
|
-
let projectEnd = 0;
|
|
725
|
-
for (const id of sortedIds) {
|
|
726
|
-
const node = taskMap.get(id)!;
|
|
727
|
-
if (node.endDate && node.endDate.getTime() > projectEnd) {
|
|
728
|
-
projectEnd = node.endDate.getTime();
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
const successors = new Map<string, string[]>();
|
|
733
|
-
for (const [id, node] of taskMap) {
|
|
734
|
-
for (const pred of node.predecessors) {
|
|
735
|
-
const succs = successors.get(pred) ?? [];
|
|
736
|
-
succs.push(id);
|
|
737
|
-
successors.set(pred, succs);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Backward pass (reverse order)
|
|
742
|
-
for (let i = sortedIds.length - 1; i >= 0; i--) {
|
|
743
|
-
// In-bounds by loop guard.
|
|
744
|
-
const id = sortedIds[i]!;
|
|
745
|
-
const node = taskMap.get(id)!;
|
|
746
|
-
const succs = successors.get(id) ?? [];
|
|
747
|
-
|
|
748
|
-
if (succs.length === 0) {
|
|
749
|
-
latestEnd.set(id, projectEnd);
|
|
750
|
-
} else {
|
|
751
|
-
let minVal = Infinity;
|
|
752
|
-
for (const succId of succs) {
|
|
753
|
-
let succLS = latestStart.get(succId);
|
|
754
|
-
if (succLS === undefined) continue;
|
|
755
|
-
|
|
756
|
-
// Reverse successor's task-level offset
|
|
757
|
-
const succTask = taskMap.get(succId)!.task;
|
|
758
|
-
if (succTask.offset) {
|
|
759
|
-
const reverseDir = (succTask.offset.direction * -1) as 1 | -1;
|
|
760
|
-
const adjusted = addGanttDuration(
|
|
761
|
-
new Date(succLS),
|
|
762
|
-
succTask.offset.duration,
|
|
763
|
-
holidays,
|
|
764
|
-
holidaySet,
|
|
765
|
-
reverseDir,
|
|
766
|
-
sprintOpts
|
|
767
|
-
);
|
|
768
|
-
succLS = adjusted.getTime();
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Reverse dep offset
|
|
772
|
-
const depOffset = depOffsetMap.get(`${id}->${succId}`);
|
|
773
|
-
if (depOffset) {
|
|
774
|
-
const reverseDir = (depOffset.direction * -1) as 1 | -1;
|
|
775
|
-
const adjusted = addGanttDuration(
|
|
776
|
-
new Date(succLS),
|
|
777
|
-
depOffset.duration,
|
|
778
|
-
holidays,
|
|
779
|
-
holidaySet,
|
|
780
|
-
reverseDir,
|
|
781
|
-
sprintOpts
|
|
782
|
-
);
|
|
783
|
-
succLS = adjusted.getTime();
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (succLS < minVal) {
|
|
787
|
-
minVal = succLS;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
latestEnd.set(id, minVal);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const duration = node.endDate!.getTime() - node.startDate!.getTime();
|
|
794
|
-
latestStart.set(id, latestEnd.get(id)! - duration);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const critical = new Set<string>();
|
|
798
|
-
for (const id of sortedIds) {
|
|
799
|
-
const node = taskMap.get(id)!;
|
|
800
|
-
const slack = (latestStart.get(id) ?? 0) - node.startDate!.getTime();
|
|
801
|
-
if (Math.abs(slack) < 86400000) {
|
|
802
|
-
critical.add(id);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
return critical;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// ── Resolved groups builder ─────────────────────────────────
|
|
810
|
-
|
|
811
|
-
function buildResolvedGroups(
|
|
812
|
-
nodes: readonly GanttNode[],
|
|
813
|
-
taskMap: Map<string, TaskNode>,
|
|
814
|
-
groups: ResolvedGroup[],
|
|
815
|
-
depth: number
|
|
816
|
-
): void {
|
|
817
|
-
for (const node of nodes) {
|
|
818
|
-
if (node.kind === 'group') {
|
|
819
|
-
const childTasks = collectTasks(node.children);
|
|
820
|
-
if (childTasks.length === 0) {
|
|
821
|
-
groups.push({
|
|
822
|
-
name: node.name,
|
|
823
|
-
color: node.color,
|
|
824
|
-
metadata: node.metadata,
|
|
825
|
-
startDate: new Date(),
|
|
826
|
-
endDate: new Date(),
|
|
827
|
-
progress: null,
|
|
828
|
-
lineNumber: node.lineNumber,
|
|
829
|
-
depth,
|
|
830
|
-
...(node.collapsed && { collapsed: true }),
|
|
831
|
-
});
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
let minStart = Infinity;
|
|
836
|
-
let maxEnd = -Infinity;
|
|
837
|
-
let totalProgress = 0;
|
|
838
|
-
let totalDuration = 0;
|
|
839
|
-
let hasProgress = false;
|
|
840
|
-
|
|
841
|
-
for (const task of childTasks) {
|
|
842
|
-
const resolved = taskMap.get(task.id);
|
|
843
|
-
if (!resolved?.startDate || !resolved?.endDate) continue;
|
|
844
|
-
if (resolved.startDate.getTime() < minStart)
|
|
845
|
-
minStart = resolved.startDate.getTime();
|
|
846
|
-
if (resolved.endDate.getTime() > maxEnd)
|
|
847
|
-
maxEnd = resolved.endDate.getTime();
|
|
848
|
-
const dur = resolved.endDate.getTime() - resolved.startDate.getTime();
|
|
849
|
-
totalDuration += dur;
|
|
850
|
-
if (task.progress !== null) {
|
|
851
|
-
totalProgress += task.progress * dur;
|
|
852
|
-
hasProgress = true;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
groups.push({
|
|
857
|
-
name: node.name,
|
|
858
|
-
color: node.color,
|
|
859
|
-
metadata: node.metadata,
|
|
860
|
-
startDate: new Date(minStart === Infinity ? 0 : minStart),
|
|
861
|
-
endDate: new Date(maxEnd === -Infinity ? 0 : maxEnd),
|
|
862
|
-
progress:
|
|
863
|
-
hasProgress && totalDuration > 0
|
|
864
|
-
? totalProgress / totalDuration
|
|
865
|
-
: null,
|
|
866
|
-
lineNumber: node.lineNumber,
|
|
867
|
-
depth,
|
|
868
|
-
...(node.collapsed && { collapsed: true }),
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
buildResolvedGroups(node.children, taskMap, groups, depth + 1);
|
|
872
|
-
} else if (node.kind === 'parallel') {
|
|
873
|
-
buildResolvedGroups(node.children, taskMap, groups, depth);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// ── Sprint band generation ──────────────────────────────────
|
|
879
|
-
|
|
880
|
-
function generateSprintBands(
|
|
881
|
-
options: GanttOptions,
|
|
882
|
-
chartStart: Date,
|
|
883
|
-
chartEnd: Date,
|
|
884
|
-
projectStart: Date
|
|
885
|
-
): ResolvedSprint[] {
|
|
886
|
-
const sprintLength = options.sprintLength!;
|
|
887
|
-
const sprintNumber = options.sprintNumber ?? 1;
|
|
888
|
-
|
|
889
|
-
// Determine anchor date: sprint-start or chart start or today
|
|
890
|
-
let anchorDate: Date;
|
|
891
|
-
if (options.sprintStart) {
|
|
892
|
-
anchorDate = new Date(options.sprintStart + 'T00:00:00');
|
|
893
|
-
} else if (options.start) {
|
|
894
|
-
anchorDate = new Date(projectStart);
|
|
895
|
-
} else {
|
|
896
|
-
const now = new Date();
|
|
897
|
-
anchorDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
898
|
-
}
|
|
899
|
-
anchorDate.setHours(0, 0, 0, 0);
|
|
900
|
-
|
|
901
|
-
// Sprint length in whole days (parser ensures only d/w with integer day result)
|
|
902
|
-
const sprintDays = Math.round(
|
|
903
|
-
sprintLength.amount * (sprintLength.unit === 'w' ? 7 : 1)
|
|
904
|
-
);
|
|
905
|
-
if (sprintDays <= 0) return []; // defensive guard
|
|
906
|
-
|
|
907
|
-
// Bail if anchor date is invalid (e.g. sprint-start 2026-13-45)
|
|
908
|
-
if (Number.isNaN(anchorDate.getTime())) return [];
|
|
909
|
-
|
|
910
|
-
// Calculate which sprint chartStart falls in, relative to the anchor
|
|
911
|
-
const chartStartTime = new Date(chartStart);
|
|
912
|
-
chartStartTime.setHours(0, 0, 0, 0);
|
|
913
|
-
const chartEndTime = new Date(chartEnd);
|
|
914
|
-
chartEndTime.setHours(0, 0, 0, 0);
|
|
915
|
-
|
|
916
|
-
const msPerDay = 86400000;
|
|
917
|
-
const dayDiff = Math.floor(
|
|
918
|
-
(chartStartTime.getTime() - anchorDate.getTime()) / msPerDay
|
|
919
|
-
);
|
|
920
|
-
// Floor division: which sprint index (from anchor) does chartStart fall in?
|
|
921
|
-
const startSprintIndex = Math.floor(dayDiff / sprintDays);
|
|
922
|
-
|
|
923
|
-
const sprints: ResolvedSprint[] = [];
|
|
924
|
-
const maxSprints = 1000; // safety guard against infinite loops
|
|
925
|
-
|
|
926
|
-
// Generate sprints that overlap with [chartStart, chartEnd]
|
|
927
|
-
// Sprint at index i (relative to anchor) has number: sprintNumber + i
|
|
928
|
-
for (let i = startSprintIndex; sprints.length < maxSprints; i++) {
|
|
929
|
-
const sprintStartDate = new Date(anchorDate);
|
|
930
|
-
sprintStartDate.setDate(sprintStartDate.getDate() + i * sprintDays);
|
|
931
|
-
sprintStartDate.setHours(0, 0, 0, 0);
|
|
932
|
-
|
|
933
|
-
const sprintEndDate = new Date(sprintStartDate);
|
|
934
|
-
sprintEndDate.setDate(sprintEndDate.getDate() + sprintDays);
|
|
935
|
-
sprintEndDate.setHours(0, 0, 0, 0);
|
|
936
|
-
|
|
937
|
-
// Stop when sprint start is past chart end
|
|
938
|
-
if (sprintStartDate.getTime() >= chartEndTime.getTime() + msPerDay) break;
|
|
939
|
-
|
|
940
|
-
sprints.push({
|
|
941
|
-
number: sprintNumber + i,
|
|
942
|
-
startDate: sprintStartDate,
|
|
943
|
-
endDate: sprintEndDate,
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
return sprints;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// ── Utility ─────────────────────────────────────────────────
|
|
951
|
-
|
|
952
|
-
function formatDate(d: Date): string {
|
|
953
|
-
const y = d.getFullYear();
|
|
954
|
-
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
955
|
-
const day = String(d.getDate()).padStart(2, '0');
|
|
956
|
-
return `${y}-${m}-${day}`;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
function findGroupOffset(
|
|
960
|
-
nodes: readonly GanttNode[],
|
|
961
|
-
groupPath: readonly string[]
|
|
962
|
-
): Offset | null {
|
|
963
|
-
if (groupPath.length === 0) return null;
|
|
964
|
-
const groupName = groupPath[groupPath.length - 1]!;
|
|
965
|
-
function search(children: readonly GanttNode[]): Offset | null {
|
|
966
|
-
for (const node of children) {
|
|
967
|
-
if (node.kind === 'group') {
|
|
968
|
-
if (node.name === groupName && node.offset) return node.offset;
|
|
969
|
-
const found = search(node.children);
|
|
970
|
-
if (found) return found;
|
|
971
|
-
} else if (node.kind === 'parallel') {
|
|
972
|
-
const found = search(node.children);
|
|
973
|
-
if (found) return found;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
return search(nodes);
|
|
979
|
-
}
|