Project Management Расписание, зависимости и ресурсы: сетевой график, сжатие срока, критическая цепь
0%

Расписание, зависимости и ресурсы: сетевой график, сжатие срока, критическая цепь

Расписание, зависимости и ресурсы: сетевой график, сжатие срока, критическая цепь

Задача «из работ, длительностей и связей получить дату» выглядит решённой в 1957 году: алгоритм критического пути линеен и умещается в тридцать строк (он разобран в обзоре трека). Проблема не в алгоритме, а в том, что его ответ систематически оптимистичен — по трём накладывающимся причинам. Резерв не то, чем кажется: резервов у работы два, и путаница роняет соседей при полной уверенности, что «запас был». В сети нет людей: CPM считает так, будто исполнителей бесконечно много, а с конечной ёмкостью задача становится NP-трудной. Длительности — распределения, а не числа: там, где сходятся ветви, вероятности перемножаются, и «некритический» путь оказывается критическим в трети прогонов.

Статья достраивает оценку и планирование (сколько работы и с каким разбросом) до расписания (когда и кем) и заканчивается тем, во что этот же предмет превратился в командах, у которых нет ни одного сетевого графика.

Часть 1. Когда расписание вообще нужно

Сетевой график дорог: его строят, поддерживают и переделывают при каждом изменении. Он окупается там, где есть жёсткие внешние даты и физически неустранимые связи — интеграция с вендором по контракту, миграция в согласованное окно, сертификация, аппаратная поставка, отключение старой системы. Он не окупается там, где работа однородна и приходит потоком: дешевле управлять потоком и прогнозировать вероятностно (Kanban и управление потоком).

Главный вывод развилки практический: прежде чем планировать вокруг зависимости, попробуйте её убрать. Согласованный заранее контракт API превращает связь «финиш→старт» в «старт→старт» и укорачивает план, фича-флаг превращает «релиз после всех» в «релиз в любой момент» (Качество и релизный менеджмент). Устранённая зависимость снижает и срок, и его разброс одновременно — ни один приём планирования так не умеет.

Часть 2. Из чего сделана сеть: работы, связи, лаги

Сначала декомпозиция: до пакетов с одним ответственным, проверяемым результатом и называемой длительностью. Правило «8/80» — пакет меньше дня означает бюрократию, больше двух недель означает, что вы не понимаете содержание. Правило 100% — сумма пакетов покрывает весь объём: самая частая причина срыва не плохие оценки, а отсутствующие строки. Затем связи; их ровно четыре типа, и разница определяет, что можно вести параллельно.

Четыре типа связи между работами: FS, SS, FF, SF и лаг

Лаг — задержка на связи (индексы догоняются двое суток после миграции). Опережение (отрицательный лаг) — разрешение стартовать до конца предшественника, то есть ставка на то, что предшественник не изменит того, на что опирается последователь. Ставка проиграла — получаете переделку.

Болезнь сети Как выглядит Чем лечить
Связь по привычке «Тестирование после разработки», хотя тесты пишутся вместе с кодом Спросить, что физически мешает начать раньше. Обычно ничего
Ресурсная связь под видом логической «B после A», потому что их делает один человек Вынести в ресурсную модель, иначе выравнивание считает неправду
Жёсткая дата вместо связи В плане «начать 12 мая», связи нет Заменить связью: даты не пересчитываются, связи пересчитываются сами
Висячая работа Нет предшественников или последователей Подключить: её резерв бесконечен, и она всегда опаздывает
700 работ на квартал План поддерживает выделенный человек Укрупнить: точность ограничена неопределённостью, а не детализацией

Сквозной пример статьи — релиз «Импорт каталога поставщика», десять пакетов.

Часть 3. Два резерва, и почему их путают

Прямой проход даёт ранние даты (ES, EF), обратный — поздние (LS, LF). Дальше начинается то, о чём обычно молчат: резервов два.

  • Общий резерв (total float, LF − EF) — насколько работу можно сдвинуть, не сдвинув дату проекта. Он общий буквально: принадлежит всему пути. Съел первый — остальным на пути не осталось.
  • Свободный резерв (free float, min(ES последователей) − EF) — насколько можно сдвинуть, не сдвинув ни одну соседнюю работу. Только он принадлежит работе лично.
from collections import deque

NET = {                                        # работа -> (длительность в днях, предшественники)
    "ANL":  (4, []),      "DB":  (3, ["ANL"]),  "BE":   (8, ["DB"]),   "PARSE": (8, ["ANL"]),
    "FE":   (9, ["ANL"]), "SEC": (2, ["BE"]),   "DOC":  (3, ["FE"]),   "PERF":  (3, ["INT"]),
    "INT":  (4, ["BE", "PARSE", "FE"]),         "REL":  (2, ["PERF", "SEC", "DOC"]),
}


def topo(net):
    """Топологический порядок (алгоритм Кана) и списки последователей. O(V + E)."""
    succ, indeg = {t: [] for t in net}, {t: 0 for t in net}
    for t, (_d, preds) in net.items():
        for p in preds:
            succ[p].append(t)
            indeg[t] += 1
    order, q = [], deque(t for t in net if indeg[t] == 0)
    while q:
        t = q.popleft()
        order.append(t)
        for s in succ[t]:
            indeg[s] -= 1
            if indeg[s] == 0:
                q.append(s)
    if len(order) != len(net):                                 # цикл = взаимная блокировка команд
        raise ValueError("в графе зависимостей цикл")
    return order, succ


def cpm(net):
    """Прямой и обратный проход: ранние даты, общий и свободный резервы, срок проекта.
    O(V + E) по времени и памяти — узкое место планирования никогда не в алгоритме."""
    order, succ = topo(net)
    es = {t: 0.0 for t in net}
    for t in order:                                            # прямой проход
        for s in succ[t]:
            es[s] = max(es[s], es[t] + net[t][0])
    ef = {t: es[t] + net[t][0] for t in net}
    end = max(ef.values())
    lf = {t: end for t in net}
    for t in reversed(order):                                  # обратный проход
        if succ[t]:
            lf[t] = min(lf[s] - net[s][0] for s in succ[t])
    total = {t: lf[t] - ef[t] for t in net}                                             # до сдвига проекта
    free = {t: (min(es[s] for s in succ[t]) if succ[t] else end) - ef[t] for t in net}  # до сдвига соседа
    return es, ef, total, free, end


es, ef, total, free, end = cpm(NET)
print(f"срок проекта: {end:.0f} дней")
for t in sorted(NET, key=lambda x: (es[x], x)):
    print(f"{t:<6} ES={es[t]:>3.0f} EF={ef[t]:>3.0f} общий={total[t]:>3.0f} свободный={free[t]:>3.0f}")
# срок проекта: 24 дней
# ... ANL, DB, BE, INT, PERF, REL — общий и свободный резервы нулевые, это критический путь
# FE     ES=  4 EF= 13 общий=  2 свободный=  0   <- ловушка
# PARSE  ES=  4 EF= 12 общий=  3 свободный=  3
# DOC    ES= 13 EF= 16 общий=  6 свободный=  6
# SEC    ES= 15 EF= 17 общий=  5 свободный=  5

Разглядывайте FE: общий резерв два дня, свободный — ноль. Менеджер, увидевший в трекере «резерв 2 дня», разрешает фронтенду задержаться на день — и немедленно уезжает DOC, потому что весь резерв FE лежит дальше по пути. Отсюда правило: с исполнителем говорят по свободному резерву, с заказчиком — по общему.

Второй вывод — про распределение резервов. Меньше 15% критических работ означает недоспецифицированный план (забыты связи), больше 60% — переспецифицированный (связи «по привычке» сделали критическим всё). Здоровая сеть на десятки работ имеет один критический путь и два-три почти критических — с резервом меньше типичной ошибки оценки, здесь это FE и SEC. За ними следят наравне с критическими: они станут критическими при первом отклонении.

Часть 4. Ресурсы: расписание без людей — это фантазия

CPM предполагает, что параллельные работы кто-то делает параллельно. В день 4 у нас одновременно активны DB, PARSE и FE — нужно три человека. Если их два, честная нижняя граница срока выглядит так:

$$T \ge \max\left(L_{\text{КП}},\ \frac{\sum_i d_i \cdot r_i}{R}\right)$$

где $L_{\text{КП}}$ — длина критического пути, $d_i$ — длительность, $r_i$ — сколько людей требует работа, $R$ — ёмкость. У нас $\max(24,\ 46/2) = 24$ дня, но граница достижима не всегда: задача с ограниченными ресурсами (RCPSP) NP-трудна (NP-полнота и приближения), поэтому используют эвристику — последовательную схему генерации расписания с приоритетом по позднему старту. Различают два режима: выравнивание (leveling) сдвигает работы, чтобы уложиться в ёмкость, и дата может уехать; сглаживание (smoothing) двигает только в пределах резервов — дата стоит, но запас исчезает.

from collections import defaultdict


def serial_sgs(net, capacity, demand):
    """Ресурсно-ограниченное расписание, последовательная схема (serial SGS).
    Приоритет — минимальный поздний старт: первыми те, у кого меньше запаса.
    O(n^2) на выбор порядка плюс O(n · H) на профиль ресурса, H — горизонт в днях.
    Эвристика: даёт допустимое расписание, оптимум не гарантирует."""
    _es, _ef, total, _free, _end = cpm(net)
    ls = {t: _es[t] + total[t] for t in net}            # поздний старт = ранний старт + общий резерв
    done, start, finish, usage = set(), {}, {}, defaultdict(int)
    while len(done) < len(net):
        ready = [t for t in net if t not in done and all(p in done for p in net[t][1])]
        t = min(ready, key=lambda x: (ls[x], -net[x][0], x))    # при равенстве — сначала длинные
        s = int(max((finish[p] for p in net[t][1]), default=0))
        d, need = int(net[t][0]), demand[t]
        while any(usage[day] + need > capacity for day in range(s, s + d)):
            s += 1                                     # ближайший день, где хватает людей
        for day in range(s, s + d):
            usage[day] += need
        start[t], finish[t] = s, s + d
        done.add(t)
    return start, finish, max(finish.values())


demand = {t: 1 for t in NET}                           # каждая работа занимает одного инженера
for capacity in (2, 3, 4):
    *_, makespan = serial_sgs(NET, capacity, demand)
    print(f"инженеров {capacity}: срок {makespan} дней")
# инженеров 2: срок 30 дней   (PARSE уезжает на 9 дней и тащит за собой INT, PERF, REL)
# инженеров 3: срок 24 дней
# инженеров 4: срок 24 дней

Три числа стоит разглядывать долго. Двое инженеров дают 30 дней вместо 24 при том же графе. Третий инженер покупает шесть дней. Четвёртый не покупает ничего: ограничение переехало с ресурса на зависимость. Это и есть содержательный ответ на «дайте ещё людей» — одна строка кода вместо спора. Отдельно про мультизадачность: посадить человека на две работы по половине дня удлиняет обе больше чем вдвое — добавляется переключение контекста и растёт незавершённая работа, а по закону Литтла время прохождения пропорционально ей.

Часть 5. Сжатие срока: сколько стоит каждый купленный день

Crashing — покупать длительность за деньги: второй разработчик на пакет, подрядчик, ускоренная поставка. У работы есть предельная длительность (ниже не сожмётся ни за какие деньги) и цена дня — наклон кривой «стоимость/время». Правило: сжимать только критические работы, только самую дешёвую, пересчитывая критический путь после каждого дня.

def crash(net, limit, price, target):
    """Жадное сжатие: покупаем по дню на самой дешёвой критической работе.
    limit — предельная (несжимаемая) длительность, price — цена дня в тыс. ₽.
    Откатываем покупку, если срок не уменьшился: значит, критическим стал параллельный путь.
    O(K · V · (V + E)), где K — число купленных дней."""
    cur = {t: [d, list(p)] for t, (d, p) in net.items()}
    *_, end = cpm(cur)
    spent, log, blocked = 0.0, [], set()
    while end > target:
        _es, _ef, total, _free, end0 = cpm(cur)
        cand = [t for t in cur if total[t] == 0 and cur[t][0] > limit[t] and t not in blocked]
        if not cand:
            break                                    # дешевле уже не будет: упёрлись
        t = min(cand, key=lambda x: (price[x], x))
        cur[t][0] -= 1
        *_, new_end = cpm(cur)
        if new_end >= end0:                          # день куплен, а проект не сократился
            cur[t][0] += 1                           # откатываем и больше не трогаем этот пакет
            blocked.add(t)
            continue
        blocked.clear()
        spent, end = spent + price[t], new_end
        log.append((t, cur[t][0]))
    return end, spent, log


LIMIT = {"ANL": 3, "DB": 2, "BE": 5, "PARSE": 6, "FE": 7, "INT": 3, "PERF": 2, "SEC": 2, "DOC": 2, "REL": 2}
PRICE = {"ANL": 60, "DB": 40, "BE": 90, "PARSE": 50, "FE": 70, "INT": 120, "PERF": 30, "SEC": 0, "DOC": 25, "REL": 0}

for target in (20, 18):
    end, spent, log = crash(NET, LIMIT, PRICE, target)
    print(f"цель {target} д -> {end:.0f} д за {spent:.0f} тыс. ₽:", " ".join(f"{t}->{d}" for t, d in log))
# цель 20 д -> 20 д за 220 тыс. ₽: PERF->2 DB->2 ANL->3 BE->7
# цель 18 д -> 19 д за 340 тыс. ₽: PERF->2 DB->2 ANL->3 BE->7 INT->3

Второй запуск показывает эффект, который ломает интуицию: первые четыре дня стоят в среднем 55 тыс. ₽ каждый, пятый (с 20 до 19) — уже 120, а шестой не покупается вовсе. К девятнадцатому дню критическими стали три пути сразу — через BE, FE и PARSE, — и день пришлось бы покупать на каждом: 90 + 70 = 160 тыс. ₽ за сутки. Жадный алгоритм честно останавливается: дальше либо принципиально другие деньги, либо другой объём. Точная задача решается как поток минимальной стоимости (потоки в сетях), но на практике важнее не оптимальность, а сам график цены дня — он превращает «нам надо на две недели раньше» в разговор о конкретной сумме.

Fast-tracking — вести последовательные работы внахлёст (FS превращается в SS с опережением). Денег не стоит, покупает срок за вероятность переделки: если параллельный запуск экономит S дней, вероятность изменения предпосылки p, а переделка стоит R дней, ставка выгодна при S > p · R. Вёрстка по правящемуся макету при p = 0,5 и R = 8 — покупка четырёх дней ожидаемой переделки за экономию трёх. Третий способ не работает вовсе: добавленный в горящий проект человек не производит с первого дня, зато потребляет время старых и квадратично увеличивает число каналов связи (Команда и коммуникации).

Часть 6. Разброс: почему сеть опаздывает, даже когда все работы «в среднем вовремя»

Здесь живёт самая недооценённая ошибка планирования — эффект слияния путей (merge bias). В точке, куда сходятся k независимых ветвей, проект продолжается, только когда закончилась последняя:

$$P(\text{узел не опоздал}) = \prod_{i=1}^{k} p_i$$

Три ветви по 0,8 — узел успевает с вероятностью 0,51. Наш INT собирает три ветви, REL — ещё три. Задержки складываются, опережения не складываются: PARSE, закончившийся на два дня раньше, не ускоряет ничего, пока ждём BE. Поэтому сумма честных оценок по сети всегда даёт оптимистичный срок, и запас внутри отдельных работ этого не лечит. Лечит симуляция по сети, а не по сумме.

import random


def simulate_network(net, three_point, trials=20_000, seed=7):
    """Монте-Карло по сети: распределение срока и индекс критичности каждой работы.
    three_point: работа -> (оптимистичная, вероятная, пессимистичная).
    O(trials · (V + E)) времени, O(V + trials) памяти; точность растёт как O(1/sqrt(trials))."""
    order, succ = topo(net)                          # топологию считаем один раз
    rnd = random.Random(seed)
    ends, critical = [], {t: 0 for t in net}
    for _ in range(trials):
        d = {t: rnd.triangular(o, p, m) for t, (o, m, p) in three_point.items()}
        es = {t: 0.0 for t in net}
        for t in order:
            for s in succ[t]:
                es[s] = max(es[s], es[t] + d[t])
        end = max(es[t] + d[t] for t in net)
        lf = {t: end for t in net}
        for t in reversed(order):
            if succ[t]:
                lf[t] = min(lf[s] - d[s] for s in succ[t])
        for t in net:
            if lf[t] - es[t] - d[t] < 1e-9:          # нулевой резерв именно в этом прогоне
                critical[t] += 1
        ends.append(end)
    ends.sort()
    return ends, {t: c / trials for t, c in critical.items()}


THREE = {
    "ANL": (3, 4, 7),  "DB": (2, 3, 6),   "BE": (6, 8, 16),  "PARSE": (5, 8, 16),
    "FE": (6, 9, 18),  "INT": (3, 4, 9),  "PERF": (2, 3, 7), "SEC": (1, 2, 5),
    "DOC": (2, 3, 6),  "REL": (1, 2, 4),
}

ends, ci = simulate_network(NET, THREE)
pct = lambda p: ends[int(p * len(ends)) - 1]
print(f"P50={pct(0.5):.1f}  P85={pct(0.85):.1f}  P95={pct(0.95):.1f}")
print("уложились в детерминированные 24 дня:", f"{sum(1 for e in ends if e <= 24) / len(ends):.1%}")
print(" ".join(f"{t}:{c:.0%}" for t, c in sorted(ci.items(), key=lambda kv: -kv[1])))
# P50=30.4  P85=33.6  P95=35.5
# уложились в детерминированные 24 дня: 0.5%
# ANL:100% PERF:100% INT:100% REL:100% DB:70% BE:70% FE:21% PARSE:9% SEC:0% DOC:0%

Разбор результата:

  1. Детерминированные 24 дня достигаются в 0,5% прогонов. Это не «оценки плохие», это устройство сети. Наружу называют P85 ≈ 34 дня, а 24 дня оставляют внутренней целью.
  2. Индекс критичности важнее критического пути. FE формально некритичен (резерв 2 дня), но критичен в 21% прогонов; DB и BE — в 70%, потому что иногда их обгоняют соседние ветви. Всё выше 15% попадает в еженедельный контроль независимо от того, что нарисовано в плане; нулевой индекс (SEC, DOC) — повод перестать тратить на работу внимание руководства.
  3. Независимость — идеализация. Общая инфраструктура, один заболевший, один сорвавшийся вендор двигают сразу несколько работ; корреляция увеличивает разброс, иногда вдвое. Корректно она добавляется дискретными рисковыми событиями поверх симуляции — см. Управление рисками.

Часть 7. Критическая цепь: буфер вместо запаса в каждой задаче

Голдратт заметил («Critical Chain», 1997), что запас, заложенный внутрь каждой оценки, до проекта не доходит. Съедают его студенческий синдром (начинаем в последний момент), закон Паркинсона (работа заполняет отведённое время), нежелание сдавать раньше (сдал раньше — в следующий раз срежут оценку) и вредная многозадачность. Плюс уже разобранное слияние путей. Механика ответа:

  1. Длительности берутся агрессивные, с вероятностью успеть около 50%.
  2. Критическая цепь — критический путь с учётом ресурсных конфликтов (выравнивание из части 4).
  3. Срезанное собирается в проектный буфер в конце цепи; впадающие ветви защищаются питающими буферами; дефицитные люди — ресурсными буферами (предупреждение «ты нужен через три дня»).
  4. Управление ведётся по расходу буфера, а не по датам отдельных задач.
from math import sqrt

# критическая цепь: (работа, оценка с вероятностью 50%, оценка с вероятностью 90%)
CHAIN = [("ANL", 4, 7), ("DB", 3, 6), ("BE", 8, 16), ("INT", 4, 9), ("PERF", 3, 7), ("REL", 2, 4)]
chain_len = sum(m for _t, m, _p in CHAIN)
cuts = [p - m for _t, m, p in CHAIN]                 # то, что срезали с каждой работы
print(f"цепь по 50%: {chain_len} д; наивная сумма оценок по 90%: {sum(p for _t, _m, p in CHAIN)} д")
print(f"буфер «половина срезанного»: {0.5 * sum(cuts):.1f} д -> обязательство {chain_len + 0.5 * sum(cuts):.1f} д")
print(f"буфер SSQ (корень из суммы квадратов): {sqrt(sum(c * c for c in cuts)):.1f} д"
      f" -> обязательство {chain_len + sqrt(sum(c * c for c in cuts)):.1f} д")
# цепь по 50%: 24 д; наивная сумма оценок по 90%: 49 д
# буфер «половина срезанного»: 12.5 д -> обязательство 36.5 д
# буфер SSQ (корень из суммы квадратов): 11.3 д -> обязательство 35.3 д

Сравните с симуляцией из части 6: обязательство по SSQ (35,3) попадает примерно в P94 распределения, «половина срезанного» (36,5) — в P98, а наивная сумма оценок «по 90%» дала бы 49 дней, почти вдвое больше медианы. Разница между 49 и ~34 — это цена перестраховки, и одновременно причина, по которой команды с раздутыми оценками всё равно опаздывают. Контроль ведут по fever chart: по горизонтали доля выполненной цепи, по вертикали доля израсходованного буфера; важна траектория, а не точка.

Fever chart: расход проектного буфера против прогресса по критической цепи

Правило вмешательства обезоруживающе простое: зелёная зона — не трогать (руководитель, «наводящий порядок» в зелёной зоне, только вредит); жёлтая — написать план восстановления; красная — исполнять его. Это редкий артефакт статус-отчётности, где действие однозначно следует из цвета.

Честная критика. Три вещи работают независимо от отношения к теории ограничений: буфер на уровне проекта, явный учёт ресурсных конфликтов, управление по тренду расхода буфера. Но рандомизированных исследований эффективности почти нет; публикуемые кейсы — из проектных организаций (стройка, ОКР, обслуживание техники), а не из продуктовой разработки; «оценки по 50%» не приживаются там, где за срыв наказывают; «одна задача на человека» ломается о дежурства. Разумно забрать буферы и fever chart, не покупая идеологию целиком — как поступают с гибридными процессами.

Часть 8. Тот же предмет без единого сетевого графика

В командах, которые никогда не рисовали сеть, всё перечисленное существует под другими именами. От переименования физика не меняется.

Классическое планирование То же в потоковом мире Что общего
Критический путь Самая длинная цепочка блокировок между командами Срок определяет цепь, а не сумма работ
Общий резерв Запас до внешнего обязательства (аудит, конференция) Принадлежит пути, а не задаче
Ресурсное выравнивание WIP-лимиты и число параллельных инициатив Ёмкость конечна, очередь удлиняет всё
Проектный буфер Разница между P50-прогнозом и озвученной датой Запас держат снаружи задач
Fast-tracking Работа по контракту API до готовности бэкенда Срок покупается за вероятность переделки
Индекс критичности Доля задач, ждавших внешней зависимости Где реально теряется время
Сетевой график релиза Доска зависимостей на PI planning Явные связи между командами

Практический минимум для потоковой команды — три метрики: доля работ, хоть раз заблокированных внешней стороной, суммарное время в блокировке (обычно больше времени активной работы) и число открытых межкомандных зависимостей. Первые две живут в метриках потока и видны в lead time из DORA; третья — в масштабировании. Структурное лекарство — не лучшее расписание, а меньше связей: разделение по ограниченным контекстам убирает зависимость навсегда, выравнивание лишь аккуратно её обходит.

Часть 9. Базовый план: что делать, когда всё поехало

Расписание бесполезно без зафиксированного базового плана — снимка, с которым сравнивают факт. Без него «мы отстаём на неделю» непроверяемо, а перепланирование каждую пятницу гарантирует, что проект никогда не опаздывает и никогда не приезжает.

Дешёвый и незаслуженно редкий инструмент контроля — анализ тренда вех: на каждом отчётном срезе фиксируется прогнозируемая дата каждой вехи, и смотрится не значение, а наклон.

Отчёт Прогноз вехи «Интеграция» Прогноз вехи «Релиз» Чтение
Неделя 2 19-й день 24-й день Плана держимся
Неделя 4 21-й день 26-й день Обе поехали на два дня — общая причина
Неделя 6 24-й день 29-й день Наклон устойчив: экстраполируйте, а не надейтесь
Неделя 8 25-й день 33-й день Линии разошлись: между вехами появилась новая работа

Горизонтальная линия — веха под контролем; устойчивый наклон — систематическая ошибка оценки, которая сама не исправится; расхождение линий — в план тихо попала незапланированная работа. Наклон, подтверждённый трижды подряд, — момент решения: сокращать объём, покупать дни (часть 5) или признавать срыв и идти в контроль изменений и критерии остановки. Правило, экономящее репутацию: сообщайте тренд, а не свершившийся факт.

Часть 10. Типичные ошибки

  1. Называть исполнителю общий резерв. Он принадлежит пути; личный запас работы — свободный резерв.
  2. Планировать загрузку на 100%. Ожидание в очереди растёт как ρ/(1−ρ): 90% вместо 80% удваивает срок.
  3. Ставить в план даты вместо связей. План с жёсткими датами не пересчитывается и мгновенно становится фикцией.
  4. Прятать ресурсные конфликты в логические связи. Выравнивание после этого считает неправду.
  5. Называть наружу детерминированную дату CPM. В нашей сети её вероятность — половина процента.
  6. Сжимать некритическую работу. Деньги потрачены, срок не изменился; классика — ускорять то, что легче ускорить.
  7. Одна оценка на пакет вместо трёх. Без разброса нельзя посчитать ни буфер, ни индекс критичности.
  8. Держать буфер внутри задач. Он гарантированно съедается студенческим синдромом и законом Паркинсона.
  9. Реагировать на зелёную зону fever chart. Вмешательство без сигнала разрушает доверие к сигналу.
  10. Перепланировать молча. Каждый пересмотр базового плана — решение спонсора и запись в журнале.
  11. Забыть приёмку, обучение поддержки и прогрев. Работы, которых нет в сети, всё равно случаются.

Часть 11. Как это выглядит в проде

Релиз интеграции с внешним платёжным провайдером; жёсткая дата — окно сертификации у вендора, сдвиг стоит квартала.

  • День 1. Декомпозиция по правилу 8/80, 22 пакета. Связи ставились по вопросу «что физически мешает начать раньше»: из 31 нарисованной связи выжили 19.
  • День 2. Три оценки на пакет, полчаса на всю сеть. CPM даёт 41 день, симуляция по сети — P50 = 48, P85 = 55. Со спонсором говорим сразу по P85: это единственный способ не пообещать невыполнимое.
  • День 3. Выравнивание по фактической ёмкости (два бэкендера, полтора фронтендера) даёт 59 дней — ограничением оказался не граф, а люди. Человек с соседней инициативы на четыре недели возвращает срок к 51 дню.
  • День 4. Индекс критичности показал, что пакет «обновление SDK провайдера» критичен в 34% прогонов при плановом резерве в четыре дня. Попал в еженедельный контроль и в итоге действительно уехал.
  • Еженедельно. Один экран: fever chart, три работы с наибольшим индексом критичности, открытые внешние зависимости с датами обещаний. Статус-отчёта на четыре страницы не существует.
  • Неделя 5 и итог. Жёлтая зона; план восстановления — сократить объём первой версии вместо сверхурочных, буфер вернулся в зелёное за две недели. 53 дня против обещанных 55.

Мини-итог

  • У работы два резерва: общий (до сдвига проекта) и свободный (до сдвига соседа). С исполнителем говорят по свободному, с заказчиком — по общему.
  • CPM линеен и оптимистичен: он не знает ни про людей, ни про разброс. Ресурсы считаются эвристикой, разброс — симуляцией по сети; ёмкость перестаёт быть ограничением скачком (третий инженер купил шесть дней, четвёртый — ноль).
  • Цена дня растёт нелинейно: пока критический путь один — 55 тыс. ₽ за сутки, когда их три — 160 тыс. ₽.
  • Слияние путей делает сумму честных оценок оптимистичной; наружу называют P85, а не дату из CPM, а индекс критичности даёт более полезный список для контроля, чем критический путь.
  • Буфер держат снаружи задач и управляют по его расходу. Лучшее расписание — то, которое не понадобилось: устранённая зависимость снижает и срок, и разброс.

Источники

  • Kelley J., Walker M. «Critical-Path Planning and Scheduling» (1959) — исходная работа по CPM.
  • PMI. «PMBOK Guide», раздел Schedule Management — типы связей, лаги, выравнивание, сжатие: pmi.org.
  • Goldratt E. «Critical Chain» (1997) — буферы, студенческий синдром, управление по расходу буфера: toc-goldratt.com.
  • Newbold R. «Project Management in the Fast Lane» (1998) — размер буфера по корню из суммы квадратов.
  • Kolisch R., Hartmann S. «Experimental Investigation of Heuristics for RCPSP» (2006) — почему приоритет по позднему старту работает лучше большинства правил: doi.org/10.1016/j.ejor.2005.01.065.
  • Vanhoucke M. «Measuring Time» (2009) — индекс критичности и чувствительность расписания.
  • Reinertsen D. «The Principles of Product Development Flow» (2009) — очереди и загрузка; Brooks F. «The Mythical Man-Month» (1975) — почему люди в горящем проекте его не ускоряют.

Что дальше

Это последняя статья трека. Она замыкает его инженерную часть: процесс из статей 01–12 определяет, как быстро вы узнаёте правду, статья 13 — на каких условиях проект живёт и умирает, а расписание — единственное место, где зависимости, ёмкость и разброс складываются в конкретную дату. Если из статьи остаётся одна мысль, пусть будет эта: дата — не число, а распределение, и управляют не датой, а размером буфера между ней и обещанием.

Куда идти дальше:

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов