Skip to content
academia.sh

Lesson 16 / 19

Contribution Process

Defining the flow from proposal to release as a state machine, tying acceptance gates to approvals, detecting invalid transitions, and finding the bottleneck by measuring stage durations.

Contents

The version and migration guide the previous lesson produced did not ask where the changes came from; the interface definition was read twice, and something had changed in between. Whose decision was it to add the width prop to button, who reviewed it, and how many days did that take?

The contribution process answers these questions. When a design system turns into something the teams using it cannot change, one of two outcomes follows: the system falls behind the need, or teams bypass it and write their own copies. The contribution process exists to prevent both, but it carries a cost of its own, and that cost must be measured.

The Stages of the Flow

A contribution passes through several distinct states from the moment it is proposed to the moment it releases, and each state has a question of its own:

  • Proposal. What is being asked for? At this stage, not a solution but a need is written: which screen, what problem, why the existing components cannot solve it.
  • Discovery. Is this really the system’s job? The scope criterion defined in the first lesson is run here; if the need is specific to a single team, the contribution is redirected to the product.
  • Draft. How will it be solved? The interface is designed, the doc sections are written, migration impact is computed if there is one.
  • Review. Does the solution meet the system’s criteria? Three separate reviews run: design, code, and accessibility.
  • Acceptance. The decision has been made. Writing the release note is pending.
  • Release. The contribution shipped in a release.

Three terminal states are added to these: rejected, deferred, and a return from review to draft. Rejection is not a failure; it is proof that the scope criterion is working. A contribution process that never rejects anything shows that the criterion is not being run.

Building the State Machine

Defining the flow as a written document is not enough; which transition is valid and which approvals a transition requires must be machine-checkable. The program below takes the history of seven contributions, checks transitions and gates, computes stage durations, and finds the bottleneck.

// contribution.mjs — contribution flow state machine, gate checks, stage durations

// Valid transitions. Any transition not in the list is invalid.
const TRANSITION = {
  proposal:  ["discovery", "rejected", "deferred"],
  discovery: ["draft", "rejected", "deferred"],
  draft:     ["review", "deferred"],
  review:    ["acceptance", "draft", "rejected"],
  acceptance: ["release"],
  release:   [],
  rejected:  [],
  deferred:  ["discovery"],
};

// Gates: the approvals that must be collected before a transition can happen.
const GATE = {
  "review->acceptance": ["design", "code", "accessibility"],
  "acceptance->release": ["release-notes"],
};

// Contribution histories: [day, state] pairs and the approvals collected by that point.
const CONTRIBUTIONS = [
  { name: "button.width", approvals: ["design", "code", "accessibility", "release-notes"],
    history: [[0, "proposal"], [2, "discovery"], [5, "draft"], [9, "review"], [12, "acceptance"], [14, "release"]] },
  { name: "dropdown.searchable", approvals: ["design", "code", "accessibility", "release-notes"],
    history: [[0, "proposal"], [6, "discovery"], [11, "draft"], [18, "review"], [47, "acceptance"], [52, "release"]] },
  { name: "number-field", approvals: ["design", "code", "accessibility", "release-notes"],
    history: [[0, "proposal"], [3, "discovery"], [10, "draft"], [21, "review"], [29, "draft"], [36, "review"], [44, "acceptance"], [46, "release"]] },
  { name: "tooltip.docs", approvals: ["design", "code", "accessibility", "release-notes"],
    history: [[0, "proposal"], [4, "deferred"], [61, "discovery"], [66, "draft"], [70, "review"], [78, "acceptance"], [80, "release"]] },
  { name: "card.clickable", approvals: ["design", "code", "accessibility", "release-notes"],
    history: [[0, "proposal"], [3, "review"], [8, "acceptance"], [10, "release"]] },
  { name: "table.density", approvals: ["design", "code", "release-notes"],
    history: [[0, "proposal"], [5, "discovery"], [9, "draft"], [16, "review"], [23, "acceptance"], [25, "release"]] },
  { name: "badge.icon-removal", approvals: ["design"],
    history: [[0, "proposal"], [7, "discovery"], [19, "rejected"]] },
];

// 1. Transition and gate checks.
console.log("invalid transitions and gate violations");
let flawed = 0;
for (const c of CONTRIBUTIONS) {
  const flaws = [];
  for (let i = 1; i < c.history.length; i++) {
    const before = c.history[i - 1][1];
    const now = c.history[i][1];
    if (!TRANSITION[before].includes(now)) flaws.push(`invalid transition: ${before} → ${now}`);
    const gate = GATE[`${before}->${now}`];
    if (gate) {
      const missing = gate.filter((o) => !c.approvals.includes(o));
      if (missing.length) flaws.push(`missing approval for ${before} → ${now}: ${missing.join(", ")}`);
    }
  }
  if (flaws.length) {
    flawed++;
    console.log(`${c.name.padEnd(23)} ${flaws.join(" | ")}`);
  }
}
console.log(`flawed contributions: ${flawed}/${CONTRIBUTIONS.length}`);

// 2. Time spent per stage.
const durations = {};
for (const c of CONTRIBUTIONS) {
  for (let i = 1; i < c.history.length; i++) {
    const state = c.history[i - 1][1];
    const duration = c.history[i][0] - c.history[i - 1][0];
    (durations[state] ??= []).push(duration);
  }
}
const median = (arr) => {
  const s = [...arr].sort((a, b) => a - b);
  const m = Math.floor(s.length / 2);
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
};

console.log("\nstage        transitions   median days   longest days   total days");
for (const state of ["proposal", "discovery", "draft", "review", "acceptance", "deferred"]) {
  const d = durations[state] ?? [];
  if (d.length === 0) continue;
  console.log(
    `${state.padEnd(12)} ${String(d.length).padStart(11)} ${String(median(d)).padStart(13)} ` +
    `${String(Math.max(...d)).padStart(14)} ${String(d.reduce((t, x) => t + x, 0)).padStart(11)}`
  );
}

// 3. End-to-end time: proposal -> release, for contributions that reached release.
const released = CONTRIBUTIONS.filter((c) => c.history.at(-1)[1] === "release");
const endToEnd = released.map((c) => c.history.at(-1)[0] - c.history[0][0]);
console.log("\ncontribution            end-to-end days");
for (const c of released) console.log(`${c.name.padEnd(23)} ${String(c.history.at(-1)[0]).padStart(15)}`);
console.log(`median end-to-end time: ${median(endToEnd)} days`);
console.log(`longest: ${Math.max(...endToEnd)} days   shortest: ${Math.min(...endToEnd)} days`);

// 4. Bottleneck: give each stage's share of the total time spent.
const totalDuration = Object.values(durations).flat().reduce((t, x) => t + x, 0);
console.log("\nstage's share of total time");
const shares = Object.entries(durations)
  .map(([state, d]) => [state, d.reduce((t, x) => t + x, 0)])
  .sort((a, b) => b[1] - a[1]);
for (const [state, t] of shares) {
  console.log(`${state.padEnd(12)} ${String(t).padStart(4)} days  ${((t / totalDuration) * 100).toFixed(1)}%`);
}
invalid transitions and gate violations
card.clickable          invalid transition: proposal → review
table.density           missing approval for review → acceptance: accessibility
flawed contributions: 2/7

stage        transitions   median days   longest days   total days
proposal               7             4              7          30
discovery              6             5             12          36
draft                  6             7             11          40
review                 7             8             29          68
acceptance             6             2              5          15
deferred               1            57             57          57

contribution            end-to-end days
button.width                         14
dropdown.searchable                  52
number-field                         46
tooltip.docs                         80
card.clickable                       10
table.density                        25
median end-to-end time: 35.5 days
longest: 80 days   shortest: 10 days

stage's share of total time
review         68 days  27.6%
deferred       57 days  23.2%
draft          40 days  16.3%
discovery      36 days  14.6%
proposal       30 days  12.2%
acceptance     15 days  6.1%

Reading the Two Flaws

Two of the seven contributions are flawed, and the two flaws differ and break different things.

card.clickable jumped straight from proposal to review; discovery and draft were skipped. The result is the shortest end-to-end time on the list: ten days. The work of the two skipped stages was never done either: the scope criterion never ran, so whether this change was the system’s job was never asked, and with no draft written, migration impact was never computed. This fast-looking path pushes its cost to later.

table.density made the transitions correctly but was accepted without accessibility approval. This overlaps with a row from the first lesson’s catalog table: table‘s accessibility review looked like it had not passed. Two audits show the same gap from two different places — one from the catalog’s state, the other from the contribution’s history.

This is where gates earn their value. Writing that a review type is “expected” is not enough; unless it becomes a condition of the transition, it is the first thing skipped when things get tight.

Where the Time Goes

The second table gives stage durations. Review is the most expensive stage both by median (8 days) and by total time share (27.6%). But total time share alone can mislead: the deferred state’s 23.2% share comes entirely from a single contribution — tooltip.docs sat deferred for 57 days.

This is why median and total are read together: total says where resources are going, median says how long a typical contribution waits. The deferred state’s median is also 57, but computed over a single observation, it is not a trend — it is a single event.

Review shows a similar spread: median 8 days, longest 29. That 29-day wait belongs to the dropdown.searchable contribution and alone explains most of the third table’s 52-day end-to-end time. The problem is not the review stage’s average but its tail; improving the process should target that long tail, not the median.

number-field’s contribution went from review back to draft, then re-entered review. This return is a valid transition and a sign the flow is working; a review that never sends a contribution back would show the review had become a formality.

The Process’s Cost and Classifying Contributions

The median end-to-end time is 35.5 days — neither good nor bad on its own; it gains meaning against a threshold: when a contribution’s time through the system exceeds the time it takes to write its own copy, teams write their own copies. If adding an optional feature takes a team half a day in its own repository, a thirty-five-day flow never brings that feature into the system at all; the system stops seeing real needs.

The fix is not removing the gates but classifying contributions. The classification built in the previous lesson applies directly here:

  • Additive contributions — a new optional prop, a widened value set — break no call. For these, discovery can be shortened and design review can be merged with code review.
  • Breaking contributions follow the full flow; a draft is not considered complete until migration cost is computed.
  • New component proposals follow the longest flow, because this is exactly where the scope criterion is actually run.

Once these three paths’ times are measured separately, the statement “our process is slow” turns into a measurable claim: in which class, at which stage, how many days.

What the Contributor Sees

The process also has a face that shows outward: the person contributing must be able to see what state their contribution is in and who the next step is waiting on. This is the state machine’s most concrete benefit — every contribution has exactly one state, and that state has an owner.

The second requirement is that the definition of done be written down. Unless the conditions for a draft moving to review — doc sections written, migration impact computed, an example added — are told to the contributor in advance, the review stage turns into producing a list of gaps instead of a decision, and the queue grows longer. Review’s 29-day longest wait is more often caused not by a slow review but by a draft entering review incomplete.

Summary

  • The contribution flow consists of proposal, discovery, draft, review, acceptance, and release; each state has its own question and its own owner.
  • Transition validity and acceptance-gate approvals are machine-checkable; when they are not checked, the stage most often skipped is a review with no condition attached to it.
  • A contribution that skips a stage looks fast, but the work of the skipped stage was never done; the cost is pushed to later.
  • Total time share tells you where resources are going, median tells you the typical wait; without reading both together, a single long wait looks like a trend.
  • When a contribution’s time through the system exceeds the time it takes to write its own copy, the system stops seeing real needs; the fix is not removing the gates but defining separate paths for additive and breaking contributions.
  • Unless the definition of done is written in advance, the review stage turns into producing a list of gaps instead of a decision, and the queue grows longer.

Next Step

This lesson’s state machine did not ask one thing: who does the review? Having all three approvals come from a single central team produces a different system than having approvals come from within the contributing teams themselves; the two differ in queue time, consistency level, and how they behave at scale. The next lesson takes up this question as a governance model: it models centralized, federated, and hybrid arrangements over demand and capacity, and computes each one’s queue time and the number of deviations it produces at two different scales.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close