Skip to content
academia.sh

Lesson 09 / 10

Change Management

Change control of scope: the same request writes 0 rounds of rework at analysis and 8 at validation, moving the item from 12 rounds to 39; on the same six requests the accept-on-arrival policy takes two and produces 8 rounds of rework while the batch-at-boundary policy takes four and keeps the request waiting 30 rounds on average; once control is removed, the plan holds for one of eight items by the second request, and a change that falls outside the team boundary delivers the same item in 28 rounds instead of 23.

Contents

The previous lesson estimated the duration of a work item and measured the estimate’s error. But what actually breaks a plan is not the estimate’s deviation: an estimate is a number about the duration of work whose scope is known to be fixed; when the scope beneath the plan changes, that number does not come out wrong — it becomes moot.

Change control is the decision path that determines at which point and at what cost a scope change enters the flow. The four measurements below turn it into a number.

The Moment a Change Arrives

The example is the course’s fictional regional library network, and the three teams that build it; both the network and the teams are fictional. TD22: module ownership splits three ways — E1 holds the branch front end, the terminal, and the membership gate; E2 holds loans and fees; E3 holds the catalog gate, reporting, and notifications. TD23: a work item passes through four stages (analysis, development, review, validation); if an item touches a second team’s module, that team takes on half a development stage plus the review. Each team works one step at a time, and once a step finishes, the item returns to the back of the queue. TD24: the initial scope is eight items and 71 rounds of work; the plan leaves the items at round 0, 6, and 12 — the scope baseline is this plan’s delivery rounds. TD25: a change sends the item back to analysis; everything done up to that point is rework, and the reversal itself is a handoff. TD26: the iteration is ten rounds, and the change budget per iteration is eight rounds; the amount deducted from the budget is the extra work plus the rework it triggers, and a request that does not fit is rejected. The flow and the two control policies are an in-process model; there is no network or clock — rounds are the unit of count.

// change/flow.mjs — the fictional library network's work item flow and change control
// (model): there is no network or database, queues are a data structure, no randomness.

export const TEAM = { "branch-front": "E1", "branch-terminal": "E1", "membership-gate": "E1",
  loan: "E2", fee: "E2", report: "E3", notification: "E3", "catalog-gate": "E3" };
export const TEAMS = [...new Set(Object.values(TEAM))];
export const ITERATION = 10, BUDGET = 8, ANALYSIS = 1;

const table = (s, f) => s.trim().split("\n").map((r) => f(r.trim().split(/\s+/)));

// Initial scope: name, module, stage rounds, plan entry.
export const SCOPE = table(`
  late-fee-threshold       fee              2,4,2,2   0
  branch-shelf-view        branch-front     2,3,1,2   0
  catalog-field-mapping    catalog-gate     1,3,1,1   0
  loan-duration-extension  loan             2,4,2,2   6
  terminal-card-read       branch-terminal  1,3,1,2   6
  monthly-loan-report      report,loan      2,3,2,1   6
  balance-notification     notification,fee 2,4,2,2  12
  membership-verification  membership-gate  2,3,1,2  12`,
([name, m, i, g]) => ({ name, module: m.split(","), work: i.split(",").map(Number), entry: +g }));

// Change: arrival round, name, target, module, extra work.
export const CHANGE = table(`
   3  fee-waiver             late-fee-threshold       fee              3
   6  shelf-filter           branch-shelf-view        branch-front     2
   9  extension-cap          loan-duration-extension  loan             3
  12  fee-column-in-report   monthly-loan-report      fee              3
  15  terminal-extension     terminal-card-read       loan             2
  18  second-field-mapping   catalog-field-mapping    catalog-gate     3`,
([t, name, target, m, e]) => ({ round: +t, name, target, module: m.split(","), extra: +e }));

// Owning team resolves and develops; a second team takes on half a development stage plus review.
export function steps(module, work) {
  const e = [...new Set(module.map((m) => TEAM[m]))], owner = e[0];
  const a = [{ name: "analysis", team: owner, work: work[0] },
    { name: "development", team: owner, work: work[1] }];
  for (const x of e.slice(1)) a.push({ name: "development", team: x, work: Math.ceil(work[1] / 2) });
  return [...a, { name: "review", team: e.at(-1), work: work[2] },
    { name: "validation", team: owner, work: work[3] }];
}

const build = (name, module, work, entry, source) => ({ name, module: [...module], base: [...work],
  step: steps(module, work), i: 0, progress: 0, entry, source, finish: null, waiting: 0,
  handoffs: 0, rework: 0 });

export const workOf = (k) => k.step.reduce((s, a) => s + a.work, 0);

// "accept" decides when the request arrives, "batch" decides at the iteration boundary; over budget is rejected.
export function run(policy, changes, settings = {}) {
  const budget = settings.budget ?? BUDGET;
  const pool = SCOPE.map((k) => build(k.name, k.module, k.work, k.entry, "scope"));
  const items = [], spent = {}, accepted = [], rejected = [], buffer = [];
  const queue = Object.fromEntries(TEAMS.map((e) => [e, []]));
  const current = Object.fromEntries(TEAMS.map((e) => [e, null]));
  const place = (k) => { items.push(k); queue[k.step[0].team].push(k); };

  function decide(d, round, iter) {
    const k = items.find((x) => x.name === d.target && x.finish === null);
    const inPlace = policy === "accept" && k;             // if not taken in place, becomes a separate item
    // Taking it in place turns work already done into rework; a separate item sets up context (ANALYSIS).
    const done = inPlace
      ? k.step.slice(0, k.i).reduce((s, a) => s + a.work, 0) + k.progress : ANALYSIS;
    const stage = inPlace ? k.step[k.i].name : k ? "boundary" : "post-delivery";
    const m = d.extra + done;                             // amount deducted from the budget
    if ((spent[iter] ?? 0) + m > budget) return rejected.push({ d, stage, m });
    spent[iter] = (spent[iter] ?? 0) + m;
    if (!inPlace) {
      const y = build(d.name, d.module, [ANALYSIS, d.extra, 1, 1], d.round, "change");
      y.rework = ANALYSIS; place(y);
      return accepted.push({ d, item: y, stage, m });
    }
    k.rework += done; k.handoffs += 1;                     // the reversal itself is a handoff
    k.module = [...new Set([...k.module, ...d.module])];
    k.base[1] += d.extra; k.step = steps(k.module, k.base); k.i = 0; k.progress = 0;
    for (const e of TEAMS) {
      if (current[e] === k) current[e] = null;
      queue[e] = queue[e].filter((x) => x !== k);
    }
    queue[k.step[0].team].push(k);
    accepted.push({ d, item: k, stage, m });
  }

  for (let round = 0; round < 80; round++) {
    pool.filter((k) => k.entry === round).forEach(place);
    for (const d of changes.filter((x) => x.round === round))
      if (policy === "accept") decide(d, round, Math.floor(round / ITERATION)); else buffer.push(d);
    if (policy === "batch" && round > 0 && round % ITERATION === 0)
      for (const d of buffer.splice(0)) decide(d, round, round / ITERATION);
    for (const e of TEAMS) if (!current[e] && queue[e].length) current[e] = queue[e].shift();
    for (const e of TEAMS) for (const k of queue[e]) k.waiting++;
    for (const e of TEAMS) {
      const k = current[e];
      if (!k || ++k.progress < k.step[k.i].work) continue;
      k.progress = 0; k.i++; current[e] = null;
      if (k.i >= k.step.length) k.finish = round + 1;
      else { k.handoffs++; queue[k.step[k.i].team].push(k); }
    }
  }
  if (items.some((k) => k.finish === null)) throw new Error("run did not finish within 80 rounds");
  return { items, accepted, rejected, end: Math.max(...items.map((k) => k.finish)) };
}
// change/measure.mjs — cost of the moment of entry, two control policies, scope creep, team boundary.
import { run, workOf, SCOPE, CHANGE, BUDGET, ITERATION } from "./flow.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const sum = (r, a) => r.items.reduce((s, k) => s + k[a], 0);
const avg = (a) => (a.reduce((s, x) => s + x, 0) / a.length).toFixed(1);
const inScope = (r) => r.items.filter((k) => k.source === "scope");

const T = run("accept", []), BASELINE = Object.fromEntries(T.items.map((k) => [k.name, k.finish]));
const BASE_WORK = T.items.reduce((s, k) => s + workOf(k), 0);
const row = (r, name) => {                        // one item's flow measures
  const k = r.items.find((x) => x.name === name);
  return [k.rework, k.handoffs, k.waiting, k.finish - k.entry, r.end];
};
console.log(`baseline plan: ${SCOPE.length} items, ${BASE_WORK} rounds of work, last delivery round ${T.end}, ` +
  `average delivery time ${avg(inScope(T).map((k) => k.finish - k.entry))} rounds; ` +
  `iteration ${ITERATION} rounds, iteration budget ${BUDGET} rounds`);

// 1. Same change, different arrival round; budget unlimited.
const TARGET = "late-fee-threshold";
const G = [-9, -15, 8, 10, 9, 15, 10];
console.log("\n1. same change (fee-waiver, 3 rounds extra work), different arrival round");
write(G, "arrival", "entry stage", "rework", "handoffs", "waiting", "delivery time",
  "flow end");
write(G, "none", "-", ...row(T, TARGET));
for (const round of [0, 8, 14]) {
  const r = run("accept", [{ round, name: "fee-waiver", target: TARGET, module: ["fee"], extra: 3 }],
    { budget: Infinity });
  write(G, round, r.accepted[0].stage, ...row(r, TARGET));
}

// 2. Two control policies, same six requests, same budget.
const R = { accept: run("accept", CHANGE), batch: run("batch", CHANGE) };
const outcome = (r, d) => {                       // the request's outcome under this policy
  const a = r.accepted.find((x) => x.d.name === d.name), x = r.rejected.find((y) => y.d.name === d.name);
  return a ? `accepted/${a.stage}/${a.m} -> ${a.item.finish}` : `rejected/${x.stage}/${x.m}`;
};
const B = [-23, 9, -34, -1];
console.log("\n2. change control: accept-on-arrival policy vs batch-at-boundary policy");
write(B, "change", "arrival", "  accept-on-arrival", "  batch-at-boundary");
for (const d of CHANGE)
  write(B, d.name, d.round, "  " + outcome(R.accept, d), "  " + outcome(R.batch, d));
const O = [-30, 22, 22];
console.log();
write(O, "measure", "accept-on-arrival", "batch-at-boundary");
const summary = (r) => ({
  "initial scope avg. delivery": avg(inScope(r).map((k) => k.finish - k.entry)),
  "finished by baseline round": `${inScope(r).filter((k) => k.finish <= BASELINE[k.name]).length}/8`,
  rework: sum(r, "rework"), waiting: sum(r, "waiting"),
  handoffs: sum(r, "handoffs"),
  "accepted / rejected": `${r.accepted.length}/${r.rejected.length}`,
  "change avg. delivery": avg(r.accepted.map((x) => x.item.finish - x.d.round)),
  "flow end": r.end,
});
const sA = summary(R.accept), sB = summary(R.batch);
for (const k of Object.keys(sA)) write(O, k, sA[k], sB[k]);

// 3. How long the baseline holds if control never rejects.
const K = [-12, 12, 16, 11, 10];
console.log("\n3. scope creep (budget unlimited: every request is accepted)");
write(K, "accepted", "added work", "added/baseline", "plan held", "flow end");
for (const n of [0, 1, 2, 6]) {
  const r = run("accept", CHANGE.slice(0, n), { budget: Infinity });
  const sc = inScope(r), added = r.items.reduce((s, k) => s + workOf(k), 0) - BASE_WORK;
  let held = 0;                                    // in plan order, up to the first miss
  for (const k of [...sc].sort((a, b) => BASELINE[a.name] - BASELINE[b.name])) {
    if (k.finish > BASELINE[k.name]) break;
    held++;
  }
  write(K, n, added, `${((100 * added) / BASE_WORK).toFixed(1)}%`, `${held}/8`, r.end);
}

// 4. Whether the change falls inside or outside the team boundary.
const M = [-28, 6, 8, 10, 9, 15, 10];
console.log("\n4. same change (terminal-card-read, round 12, 2 rounds extra work): where it falls");
write(M, "where it falls", "step", "rework", "handoffs", "waiting", "delivery time", "flow end");
const NAME = "terminal-card-read";
write(M, "no change", T.items.find((k) => k.name === NAME).step.length, ...row(T, NAME));
for (const [where, module] of [["inside the team boundary", ["branch-terminal"]],
  ["outside the team boundary", ["loan"]]]) {
  const r = run("accept", [{ round: 12, name: "terminal-change", target: NAME, module, extra: 2 }],
    { budget: Infinity });
  write(M, where, r.items.find((k) => k.name === NAME).step.length, ...row(r, NAME));
}
baseline plan: 8 items, 71 rounds of work, last delivery round 30, average delivery time 12.9 rounds; iteration 10 rounds, iteration budget 8 rounds

1. same change (fee-waiver, 3 rounds extra work), different arrival round
arrival  entry stage      rework  handoffs  waiting  delivery time  flow end
none     -                     0         3        2             12        30
0        analysis              0         4        8             21        33
8        validation            8         7       18             39        39
14       post-delivery         0         3        2             12        34

2. change control: accept-on-arrival policy vs batch-at-boundary policy
change                   arrival  accept-on-arrival                 batch-at-boundary
fee-waiver                     3  accepted/development/6 -> 22      accepted/boundary/4 -> 41
shelf-filter                   6  rejected/validation/8             accepted/post-delivery/3 -> 26
extension-cap                  9  rejected/analysis/3               rejected/boundary/4
fee-column-in-report          12  accepted/development/8 -> 36      accepted/boundary/4 -> 44
terminal-extension            15  rejected/validation/7             accepted/post-delivery/3 -> 45
second-field-mapping          18  rejected/post-delivery/4          rejected/post-delivery/4

measure                            accept-on-arrival     batch-at-boundary
initial scope avg. delivery                     16.3                  16.3
finished by baseline round                       4/8                   3/8
rework                                             8                     4
waiting                                           44                   133
handoffs                                          31                    38
accepted / rejected                              2/4                   4/2
change avg. delivery                            21.5                  30.0
flow end                                          36                    45

3. scope creep (budget unlimited: every request is accepted)
accepted      added work  added/baseline  plan held  flow end
0                      0            0.0%        8/8        30
1                      3            4.2%        2/8        34
2                      5            7.0%        1/8        34
6                     23           32.4%        1/8        44

4. same change (terminal-card-read, round 12, 2 rounds extra work): where it falls
where it falls                step  rework  handoffs  waiting  delivery time  flow end
no change                        4       0         3        4             11        30
inside the team boundary         4       4         6       10             23        30
outside the team boundary        5       4         7       12             28        34

The numbers are in the measurement class; their inputs are the assumptions above. The first table sends the same request into the same flow at three different moments. A request arriving during analysis writes zero rounds of rework; the same request arriving during validation brings eight rounds of rework, four extra handoffs, and sixteen extra rounds of waiting: delivery time rises from 12 rounds to 39, and three rounds of extra work cost twenty-seven rounds. Arriving early is not free either — the growing item falls behind the ones released after it and finishes in 21 rounds. If the item has already been delivered, the request becomes a separate item: the target stays at its planned 12 rounds, and the flow end moves from 30 to 34. The cost leaves the item and moves into the flow.

Batch-at-Boundary versus Accept-on-Arrival

The second measurement runs the same six requests through the same budget in two policies: the accept-on-arrival policy takes the request into the item itself, the batch-at-boundary policy finishes the item according to plan and takes the request in as a separate item at the boundary. The accept-on-arrival policy takes two and rejects four. Because the amount deducted from the budget also includes rework, a request arriving at an item that is in validation eats the entire eight-round budget; the expensive request that fills the budget also gets the cheap one behind it rejected: the three-round request at round nine does not fit while its target is in analysis. The batch-at-boundary policy takes four and cuts rework in half (4 instead of 8); its cost is the value it delays — the average from arrival to delivery is 30 rounds, versus 21.5 under accept-on-arrival. Two of the four requests it takes arrive at the boundary after the target item has already finished. The initial scope’s average delivery time comes out to 16.3 rounds under both policies; the average hides the split: items finished by their baseline round are 4 of 8 against 3 of 8 — under accept-on-arrival the delay concentrates on the items that changed, under batch-at-boundary it spreads across all of them.

Scope Creep and the Baseline’s Lifespan

TD27: the third measurement removes control, and every request is accepted. Six requests add 23 rounds to 71 rounds of work (a 32.4 percent scope creep), but the baseline does not degrade in proportion to that rate. In plan order, the count of items that still hold up to the first miss drops from eight to two after one request, to one after the second, and stays there through the sixth. The added work keeps growing, and the baseline’s descriptive power bottoms out at the second change. Past that point the scope baseline is not a plan but a date: it does not say which item finishes when, it says what was once believed.

Which Boundary a Change Falls Into

The fourth measurement sends two requests of the same size into the same item at the same round; the only difference is which module they touch. The request that stays inside the boundary preserves the item’s four steps; the one that falls outside turns it into an item that two teams touch: the step count rises to five, handoffs from six to seven, waiting from 10 to 12, delivery time becomes 28 rounds instead of 23, and the flow end moves from 30 to 34. Rework is four rounds in both cases; the difference comes entirely from crossing the boundary. A request that does not overlap the team boundary is expensive, and what grows the difference is not the request itself but how module ownership is split.

Summary

  • The same request writes zero rounds of rework at analysis and eight at validation; delivery time rises from 12 rounds to 39. A request that arrives at an already delivered item becomes a separate item.
  • On the same six requests and the same budget, the accept-on-arrival policy takes two and the batch-at-boundary policy takes four; rework is 8 versus 4, waiting is 44 versus 133, and the flow end is 36 versus 45. The batch-at-boundary policy’s cost is the value it delays: the average from arrival to delivery is 30 rounds versus 21.5 under accept-on-arrival; the initial scope’s average is 16.3 rounds under both (finished by baseline round: 4 of 8 versus 3 of 8).
  • Once control is removed, six requests grow the scope by 32.4 percent, but the plan breaks even earlier: from 8 of 8 to 2 of 8 after the first request, to 1 of 8 after the second.
  • A request that falls outside the team boundary adds one step, one handoff, and two rounds of waiting; delivery time is 28 rounds instead of 23.

Next Step

Change has been controlled, the scope baseline has been kept, and its cost has been counted in rework and waiting. But every one of these measurements stops the counter the moment the item is delivered. In the model, delivery is the last step finishing; in the field it is where the software’s real lifetime begins: which team a call at night goes to, and what the work that cannot be handed over costs, appear in none of these tables. The next lesson carries the counter past delivery: what items make up the responsibility between the team that builds and the team that operates, and who is left holding the item that cannot be handed over?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close