← Commit history

0.43.0: finish-to-start now schedules back to back, with no rendered gap between dependent bars

Drew Owens ·2821abbb14 · ·parent 633a79e
6 files changed +126−18
README.md+2−2
@@ -4,9 +4,9 @@ Interactive Gantt chart viewer for project timelines. Data-driven, dark by defau  ## Features -**Auto-schedule the whole plan.** One press of **Auto-schedule** lays every task out so that it starts on the first business day AFTER its latest dependency ends, and so that nothing starts or ends on a Saturday or a Sunday. Tasks move earlier as well as later: slack is pulled in and a conflict is pushed out. A task with no dependencies is an anchor and keeps its start, since that is the one thing the plan cannot derive. Durations are preserved in BUSINESS days, so a five-day task laid across a weekend still takes five working days and finishes on the Monday rather than the Saturday. Milestones move off a weekend to the next business day and sit on or after the day their last dependency ends. The dependency graph crosses sections freely and so does the schedule. A cycle is left exactly as it is, named in the toast, because there is no honest answer for a row that waits on itself. It is one Ctrl+Z for the whole pass and one line in the changelog, and pressing it on an already aligned plan reports "0 tasks moved" and writes nothing.+**Auto-schedule the whole plan.** One press of **Auto-schedule** lays every task out BACK TO BACK with the work it waits on, and so that nothing starts or ends on a Saturday or a Sunday. A successor starts ON its latest dependency's end, so the two bars meet on the chart with no column of white between them. Tasks move earlier as well as later: slack is pulled in and a conflict is pushed out. A task with no dependencies is an anchor and keeps its start, since that is the one thing the plan cannot derive. Durations are preserved in BUSINESS days, so a five-day task laid across a weekend still takes five working days: a predecessor that ends on the Friday hands off on the Friday, and the weekend sits inside the successor's span rather than between the two of them. Milestones move off a weekend to the next business day and sit on the day their last dependency ends. The dependency graph crosses sections freely and so does the schedule. A cycle is left exactly as it is, named in the toast, because there is no honest answer for a row that waits on itself. It is one Ctrl+Z for the whole pass and one line in the changelog, and pressing it on an already aligned plan reports "0 tasks moved" and writes nothing. -**Critical path.** **Critical** lights up the chain of work with no slack in it, the one that decides the finish date: the bars, the dependency arrows along it and any milestone on it glow white and breathe, in a saturated indigo on paper. Classic CPM over business days, on the dates the plan has right now, so it is independent of Auto-schedule and answers the question about the plan as it stands. Everything else stays exactly as it was, selection and dependency mode included, and the glow recomputes as you edit.+**Critical path.** **Critical** lights up the chain of work with no slack in it, the one that decides the finish date: the bars, the dependency arrows along it and any milestone on it glow white and breathe, in a saturated indigo on paper. Classic CPM over business days, on the dates the plan has right now, so it is independent of Auto-schedule and answers the question about the plan as it stands. It uses the same finish-to-start lag Auto-schedule lays the plan out with, so the two buttons can never disagree: align the plan and the chain it aligned reads as zero float. Everything else stays exactly as it was, selection and dependency mode included, and the glow recomputes as you edit.  **Analytics.** A third lens, next to Gantt and Table. Total effort for the plan in hours (business days times eight, a forty-hour week), hours per person with shares and the unassigned bucket, status counts overall and per person, per-section totals with the range each section actually covers, milestones with days from today, dependency stats with a cycle warning, and everything overdue. Read-only, whole-plan, and re-derived from the same rows every time you switch to it. 
SKILL.md+2−2
@@ -217,8 +217,8 @@ rather than a category. - **Text size** A-minus/A+ in the header: seven steps from 85% to 150%, applies to the chart and the table, persists in `localStorage` as `prefs.fs`. Row and bar geometry scale with the type - **Fit** button beside the cell-size picker: picks the finest grain whose whole span fits the pane, so the plan is fully visible with no horizontal scrolling. Chart-only - **Gantt / Table / Analytics toggle** pinned first in the header, left of every conditional control so its position never shifts. Three lenses over the same items: the chart answers "when", the table answers "whose, and by when", and analytics answers "how much, and is any of it in trouble". See The table view and The analytics view below-- **Auto-schedule** button, with the plan-level controls next to Fit, in the chart and the table lenses. One press lays the WHOLE plan out under two rules at once: every task with dependencies starts on the first BUSINESS day after its latest predecessor ends, and no task starts or ends on a Saturday or a Sunday. Tasks move EARLIER as well as later, so slack is pulled in and a conflict is pushed out. A task with no dependencies is an ANCHOR and keeps its start (shifted off a weekend if it was on one), because that is the one thing in a plan this cannot derive. Duration is preserved in BUSINESS days, not calendar days, so a five-day task laid across a weekend still takes five working days and finishes on the Monday. Milestones take the same rule with no duration: off a weekend to the next business day, and on or after the first business day after their last dependency ends. The graph crosses sections freely and so does the schedule; a section's displayed range just follows its rows. A dependency CYCLE is left exactly as it is, along with anything downstream of it, and the toast names the rows: there is no honest answer for a row that waits on itself. The whole pass is ONE undo entry and ONE changelog event (`action: "auto-schedule"`, with a count and the names of what moved), and a press that moves nothing writes nothing. Deterministic. Disabled, with a tooltip that says why, when the plan has no dependencies-- **Critical** button, next to it: a toggle that lights up the chain of work with no slack in it, the one that decides the finish date. Classic CPM forward and backward over BUSINESS days, on the dates the plan has right now, so it is independent of Auto-schedule and never moves anything. Total float of zero is critical; only rows in the dependency graph can be. The bars, the arrows along the path and any milestone on it glow white and breathe on a 2.6s cycle, in a saturated indigo in light mode and on paper, and the table lens marks the same rows on their left edge. Purely additive: selection outlines, dependency-mode amber and armed red all still read on a glowing row, and hit-testing is untouched. It recomputes live as dates and dependencies change. Disabled with the same reasoning when the plan has no dependencies+- **Auto-schedule** button, with the plan-level controls next to Fit, in the chart and the table lenses. One press lays the WHOLE plan out under two rules at once: every task with dependencies starts BACK TO BACK with its latest predecessor, on that predecessor's end date, and no task starts or ends on a Saturday or a Sunday. Back to back is a statement about the CHART: a bar runs from its start to its end and the end is the boundary it stops at, so a successor starting on that date draws flush against it with no gap and no overlap. Tasks move EARLIER as well as later, so slack is pulled in and a conflict is pushed out. A task with no dependencies is an ANCHOR and keeps its start (shifted off a weekend if it was on one), because that is the one thing in a plan this cannot derive. Duration is preserved in BUSINESS days, not calendar days, so a five-day task laid across a weekend still takes five working days: a predecessor ending on the Friday hands off on the Friday and the weekend falls inside the successor's own span, never between the two. Milestones take the same rule with no duration: off a weekend to the next business day, and on the day their last dependency ends. The graph crosses sections freely and so does the schedule; a section's displayed range just follows its rows. A dependency CYCLE is left exactly as it is, along with anything downstream of it, and the toast names the rows: there is no honest answer for a row that waits on itself. The whole pass is ONE undo entry and ONE changelog event (`action: "auto-schedule"`, with a count and the names of what moved), and a press that moves nothing writes nothing. Deterministic. Disabled, with a tooltip that says why, when the plan has no dependencies+- **Critical** button, next to it: a toggle that lights up the chain of work with no slack in it, the one that decides the finish date. Classic CPM forward and backward over BUSINESS days, on the dates the plan has right now, so it is independent of Auto-schedule and never moves anything. It uses the SAME finish-to-start lag Auto-schedule schedules with (a successor's earliest start is its predecessor's early finish, not the day after), so the two buttons can never disagree about the same plan: auto-schedule it and the chain that was just laid out tight reads as zero float. Total float of zero is critical; only rows in the dependency graph can be. The bars, the arrows along the path and any milestone on it glow white and breathe on a 2.6s cycle, in a saturated indigo in light mode and on paper, and the table lens marks the same rows on their left edge. Purely additive: selection outlines, dependency-mode amber and armed red all still read on a glowing row, and hit-testing is untouched. It recomputes live as dates and dependencies change. Disabled with the same reasoning when the plan has no dependencies - **Click legend** to filter phases, milestones, dependency arrows, or the today line - **Click a bar** to open the date editor with start/end pickers and dependency management - **Drag bar edges** to resize (changes start or end date). Every timeline drag snaps to the grid the axis is currently drawing: day boundaries at the day cell size, week starts at weeks, month, quarter and year firsts at the coarser ones. Resizing the start leaves the end where it is and the mirror for the end; dragging the bar body snaps the START and preserves the DURATION, so the end lands at start plus the same number of days. The preview snaps live, line to line, and is exactly what gets committed. The shortest bar a resize can produce is the first grid line past the fixed edge, so it is never zero or negative. Milestones snap the same way. Dates set explicitly (table cells, the item modal, the batch editor, the project-start shift) are taken exactly as given and are never rounded
docs/BUILD-NOTES.md+86
@@ -3,6 +3,92 @@ Decisions taken while building, and the places where the plan and the code disagreed. Newest first. +## Build notes, 2026-08-19 (0.43.0, finish-to-start means back to back)++One line from Drew: "auto schedule is leaving a one-day gap between tasks w/ deps, please+close that gap so tasks are back to back. This is a really tight schedule." One focused+change, in `gantt.html` only. `server.js` did not move: the changelog sentence it renders+("moving N item(s) so every dependency lines up finish-to-start on business days") is+still exactly true, and a sentence that is still true is not a reason to redeploy a file.+**No live server was restarted and no live plan was opened at any point.**++### What the gap actually was++0.42.0 started a successor on the first business day AFTER its latest predecessor ended.+That is the textbook wording, and against this app's renderer it is off by one day.++A bar is drawn from `toX(start)` to `toX(end)`. `toX` returns the LEADING edge of a+date's cell, so a bar's right edge sits on the leading edge of its end date and the end+date is the boundary the work stops at rather than a cell the bar paints. Start the+successor one day later than that boundary and the chart draws a whole empty column+between the two bars. Measured on the bench at the day grain, where a column is 18px:++| pair | 0.42.0 pred right | 0.42.0 succ left | 0.42.0 gap | 0.43.0 gap |+|---|---|---|---|---|+| `a1 -> a2` (pred ends Friday) | 470px | 524px | **54px, 3 columns** | **0px** |+| `a2 -> a3` | 596px | 650px | **54px, 3 columns** | **0px** |+| `a3 -> a4` (mid-week) | 668px | 686px | **18px, 1 column** | **0px** |+| `c1 -> c2` | 1478px | 1532px | **54px, 3 columns** | **0px** |+| `c2 -> c4` | 1730px | 1784px | **54px, 3 columns** | **0px** |+| `a4 -> "A6 gate"` (milestone) | 794px | 812px | **18px, 1 column** | **0px** |+| `c4 -> "C5 ship"` (milestone) | 1856px | 1910px | **54px, 3 columns** | **0px** |++One BUSINESS day of gap every time, which the chart draws as one column mid-week and as+three across a weekend. That is what Drew was looking at.++### The convention 0.43.0 holds++**A successor starts ON its latest predecessor's end.** `nextBiz(latest)` where 0.42.0+had `nextBiz(addDays(latest,1))`. `nextBiz` still earns its place: a scheduled end is+always a business day so it is the identity there, but a predecessor left where it was by+a dependency cycle can end on a Saturday.++**The weekend is not a gap, and it never was one.** Under this convention a dependency+pair can never have a weekend between it, because the successor starts on the day the+predecessor ends. A predecessor ending on the Friday hands off on the Friday, and the+weekend lands INSIDE the successor's own span: `A2 slack` runs 2026-01-09 to 2026-01-15,+six calendar days wide and five business days long. That is the whole reason duration is+counted in business days.++**A dependent milestone sits on its predecessor's end**, on the leading edge of the cell+that bar stops at, so its dashed rule touches the bar rather than standing a column clear+of it.++**The CPM lag moved with it, and had to.** The critical path is a second opinion about+the same plan, and a second opinion computed with a different lag is just a disagreement:+press Auto-schedule and the critical path would have reported float on the chain it had+just laid out tight. So the forward pass takes `ES = max(own, EF(pred))` rather than+`EF(pred)+1`, the backward pass takes `LF = min(LS(succ))` rather than `LS(succ)-1`, and+an edge is critical when `EF(pred) === ES(succ)`. Proven on the bench: press Auto-schedule+and then the critical path, and `c1 -> c2 -> c4 -> C5 ship` all carry zero float while+`c3` keeps its five days, which is the same answer 0.42.0 gave about a plan laid out its+own way.++**Everything else is unchanged.** Durations in business days, no start or end on a+Saturday or a Sunday, anchors untouched, cycles left alone and named, one group undo, one+changelog event, and a second press on an aligned plan that reports "0 tasks moved" and+writes nothing. No analytics figure derives from the lag, only from spans, and no span+changed length.++### The one place the pixels are not a hard zero++Bars are floored at 6px so a one-day task stays clickable at the year grain. At the+coarser grains a short bar's right edge therefore overshoots its own end date by a pixel+or two and OVERLAPS its successor slightly. That is the existing minimum-width floor, not+the schedule, and an overlap is not the thing Drew reported. The suite says so explicitly:+exact zero at the day grain where nothing is floored, zero within a twentieth of a pixel+at the other grains for every unfloored pair, and never a positive gap for a floored one.++### The bench moved, on purpose++`mk42.mjs`'s C chain shipped pre-aligned under the OLD convention, which is what made it+the control for "Auto-schedule must not move an aligned plan" and the fixture for the CPM+hand-computation. Under the new convention that same chain has gaps in it, so it was+re-dated back to back: `c2` 2026-03-06 to 2026-03-19, `c3` 2026-03-06 to 2026-03-12, `c4`+2026-03-19 to 2026-03-25, `C5 ship` 2026-03-25. The four durations are the business days+they always were (5, 10, 5, 5), so every analytics number over that fixture is unmoved and+`c3` still hangs off the chain with exactly five days of float.+ ## Build notes, 2026-08-18 (0.42.0, the plan schedules itself, shows its critical path, and adds up)  Four things from Drew. Three of them are in the client; the fourth is one branch of the
gantt.html+33−11
@@ -5473,11 +5473,26 @@ const planHasDeps=()=>depGraph().edges>0;  // ── Auto-schedule ──────────────────────────────────────────────────────────── //-// One pass over the whole plan: every task with dependencies starts on the first-// business day AFTER its latest predecessor ends, and no task starts or ends on a-// Saturday or a Sunday. Both directions: slack is pulled in and a conflict is-// pushed out, because a schedule that only ever moved work later would drift the-// plan out one round at a time and never bring it back.+// One pass over the whole plan: every task with dependencies starts BACK TO BACK+// with its latest predecessor, and no task starts or ends on a Saturday or a+// Sunday. Both directions: slack is pulled in and a conflict is pushed out,+// because a schedule that only ever moved work later would drift the plan out one+// round at a time and never bring it back.+//+// BACK TO BACK is a statement about the DRAWN chart, and the chart is what a reader+// is looking at when they say two tasks have a gap between them. A bar runs from+// toX(start) to toX(end), so its right edge is the LEADING edge of the end date's+// cell: the end date is the boundary the work finishes on, not a cell the bar+// paints. The successor's bar therefore meets the predecessor's exactly when the+// successor's start IS the predecessor's end, and one calendar day later leaves a+// visible column of white between them, which is what 0.42.0 did and what 0.43.0+// stopped doing. The weekend never enters it: a predecessor ending on the Friday+// hands off on the Friday, and its successor's five business days still run Mon to+// Fri of the following week because addBiz counts working days, not calendar ones.+//+// nextBiz still guards the handoff. A scheduled end is always a business day, so it+// is the identity there; it earns its place for a predecessor left where it was by+// a cycle, whose end may be a Saturday. // // A task with no predecessors is an ANCHOR and keeps the start it has. That is the // only thing in the plan this cannot derive, so it is the only thing it does not@@ -5498,14 +5513,15 @@ function autoScheduleCompute(){   order.forEach(n=>{     let s;     if(n.preds.length){-      // The first business day after the LATEST predecessor end. A predecessor left-      // unscheduled because it is in a cycle contributes the date it already has.+      // The LATEST predecessor end, taken as the successor's start so the two bars+      // meet. A predecessor left unscheduled because it is in a cycle contributes+      // the date it already has, which is the one case that can be a weekend.       let latest=null;       n.preds.forEach(p=>{         const pe=plan.has(p)?plan.get(p).end:p.end;         if(latest===null||dayNum(pe)>dayNum(latest))latest=pe;       });-      s=nextBiz(addDays(latest,1));+      s=nextBiz(latest);     }else{       s=nextBiz(n.start);     }@@ -5571,6 +5587,12 @@ function cycleNames(stuck){ // happens to run past everything else is not a chain through anything. Durations // are inclusive business days, so a chain's length is the sum of its rows' working // days and a milestone contributes nothing.+//+// The finish-to-start LAG here is the one Auto-schedule uses, and it has to be, or+// the two buttons would answer different questions about the same plan: press one+// and the other would report float on the chain it had just laid out tight. A+// successor starts ON its predecessor's end, so the earliest a successor can start+// is EF(pred) and not EF(pred)+1, and the mirror holds on the backward pass. function criticalSet(){   const g=depGraph();   const inGraph=g.nodes.filter(n=>n.preds.length||n.succs.length);@@ -5586,7 +5608,7 @@ function criticalSet(){   const idx=ds=>bizBetween(base,nextBiz(ds))-1;   live.forEach(n=>{     let es=idx(n.start);-    n.preds.forEach(p=>{ if(EF.has(p))es=Math.max(es,EF.get(p)+1); });+    n.preds.forEach(p=>{ if(EF.has(p))es=Math.max(es,EF.get(p)); });     ES.set(n,es);     EF.set(n,es+Math.max(dur.get(n),1)-1);   });@@ -5594,7 +5616,7 @@ function criticalSet(){   for(let i=live.length-1;i>=0;i--){     const n=live[i];     let lf=finish;-    n.succs.forEach(s=>{ if(LS.has(s))lf=Math.min(lf,LS.get(s)-1); });+    n.succs.forEach(s=>{ if(LS.has(s))lf=Math.min(lf,LS.get(s)); });     LF.set(n,lf);     LS.set(n,lf-Math.max(dur.get(n),1)+1);   }@@ -5605,7 +5627,7 @@ function criticalSet(){   // zero-float chains that happen to touch would light up the link between them.   const edges=new Set();   crit.forEach(n=>n.preds.forEach(p=>{-    if(crit.has(p)&&EF.get(p)+1===ES.get(n)&&p.id&&n.id)edges.add(p.id+'>'+n.id);+    if(crit.has(p)&&EF.get(p)===ES.get(n)&&p.id&&n.id)edges.add(p.id+'>'+n.id);   }));   return {crit,edges,cycle:stuck,float:flt,finish,graph:g}; }
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.42.0",+  "version": "0.43.0",   "description": "Interactive Gantt chart viewer for project timelines, with a table view and an analytics page over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. One click auto-schedules the whole plan so every dependency lines up finish-to-start on business days, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for hours per person, status, per-section totals, milestones, dependency stats and what is overdue. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",   "main": "server.js",   "bin": {
page.json+2−2
@@ -2,7 +2,7 @@   "slug": "adom-gantt",   "title": "Adom Gantt",   "type": "app",-  "version": "0.42.1",+  "version": "0.43.0",   "description": "Interactive Gantt chart viewer for project timelines, with a table view and an analytics page over the same data. Data-driven, dark by default with a print-friendly light mode, and fully editable in the browser: drag bars to move or resize, multi-select and batch-shift dates, manage dependency arrows inline, reorder rows, filter by phase, and undo. One click auto-schedules the whole plan so every dependency lines up finish-to-start on business days, and one more lights up the critical path. Toggle to the table for every task and milestone as a row with owner, deadline and status, filtered per owner and edited in place, or to analytics for hours per person, status, per-section totals, milestones, dependency stats and what is overdue. Every edit is recorded in an append-only changelog so an AI can read what a human changed and build on it. Imports JSON, CSV, MS Project XML and Jira CSV; exports JSON, CSV, SVG and a self-contained HTML snapshot.",   "main": "server.js",   "bin": {@@ -81,4 +81,4 @@     "name": "Drew Owens",     "email": "[email protected]"   }-}\ No newline at end of file+}