Lesson 04 / 19
Building from Scratch and from Existing
Comparing the from-scratch and from-existing strategies on the same inventory for migration cost and scale quality, building the hybrid path with an exception log, and freeze-based phased migration.
Contents
The visual audit delivered the inventory: 52 distinct values across four products, 17 of them unearned variety. The inventory says what is used; it does not say what should be used. This lesson builds the transition between the two and shows that it can be made in two different ways.
Building from scratch derives the scale independently of the inventory, from the criteria in the Spacing Scale and Typographic Scale lessons; it then fits the existing values onto that scale. Building from existing declares the most-used values a scale; the scale is a summary of the inventory. The two paths produce different migration costs and different scale quality, and these two quantities are independent of each other.
Two Strategies, One Inventory
The comparison is made across three numeric property classes: spacing, font size, and corner radius. Color is treated separately, because color’s scale is not a sequence of numbers but a role table, and its migration is measured by a matching check, not by drift.
Each strategy is evaluated with two sets of criteria. Scale quality: the number of steps, the number of neighbor pairs that fall below the distinguishability threshold, and the number of neighbor pairs whose ratio reaches 2 or more, the requirement for grouping. Migration cost: how many uses have their value shifted, how many of those shifts exceed the distinguishability threshold, and what the largest shift is.
// strategy.mjs — comparing two starting strategies on the same inventory // Inventory extracted in the Visual Audit lesson: value -> usage count. const INVENTORY = { spacing: { 2: 1, 4: 1, 6: 4, 8: 9, 10: 4, 12: 6, 14: 3, 16: 5, 18: 2, 20: 3, 24: 5, 28: 1, 32: 1 }, "font size": { 12: 4, 13: 7, 14: 4, 15: 2, 16: 1, 18: 1, 20: 1, 22: 1, 24: 1 }, "corner radius": { 3: 3, 4: 5, 6: 2, 8: 3, 12: 1 }, }; const THRESHOLD = { spacing: 2, "font size": 1, "corner radius": 2 }; // Strategy A: the scale is built from principles. The spacing scale is a hybrid scale // (Spacing Scale lesson); font size is a base-16 scale with a 1.25 ratio (Typographic Scale lesson). const FROM_SCRATCH = { spacing: [4, 8, 12, 16, 24, 32, 48, 64, 96], "font size": [-1, 0, 1, 2, 3].map((n) => Math.round(16 * Math.pow(1.25, n))), "corner radius": [4, 8], }; // Strategy B: the scale is derived from existing values. The most-used values are // selected until they cover a set share of usage. const COVERAGE_TARGET = 0.8; function fromExisting(distribution, target) { const total = Object.values(distribution).reduce((t, v) => t + v, 0); const sorted = Object.entries(distribution) .map(([d, n]) => [Number(d), n]) .sort((a, b) => b[1] - a[1] || a[0] - b[0]); const selected = []; let accumulated = 0; for (const [d, n] of sorted) { if (accumulated / total >= target) break; selected.push(d); accumulated += n; } return selected.sort((a, b) => a - b); } const FROM_EXISTING = Object.fromEntries( Object.entries(INVENTORY).map(([s, d]) => [s, fromExisting(d, COVERAGE_TARGET)]) ); // --- criteria calculations ---------------------------------------------------- const nearest = (d, scale) => scale.reduce((a, b) => (Math.abs(b - d) < Math.abs(a - d) ? b : a)); function scaleQuality(scale, threshold) { let indistinguishable = 0; let grouping = 0; for (let i = 1; i < scale.length; i++) { if (scale[i] - scale[i - 1] <= threshold) indistinguishable++; if (scale[i] / scale[i - 1] >= 2) grouping++; } return { steps: scale.length, indistinguishable, grouping }; } function migrationCost(distribution, scale, threshold) { let usage = 0; let shifted = 0; let visible = 0; let largest = 0; let totalShift = 0; for (const [d, n] of Object.entries(distribution)) { const value = Number(d); const target = nearest(value, scale); const shift = Math.abs(target - value); usage += n; if (shift > 0) shifted += n; if (shift > threshold) visible += n; totalShift += shift * n; largest = Math.max(largest, shift); } return { usage, shifted, visible, largest, avgShift: totalShift / usage }; } for (const [name, scales] of [["A from scratch", FROM_SCRATCH], ["B from existing", FROM_EXISTING]]) { console.log(`--- ${name} ---`); console.log("property scale steps indistinguishable grouping"); for (const cls of Object.keys(INVENTORY)) { const k = scaleQuality(scales[cls], THRESHOLD[cls]); console.log( `${cls.padEnd(16)} ${scales[cls].join(",").padEnd(30)} ${String(k.steps).padStart(5)} ` + `${String(k.indistinguishable).padStart(18)} ${String(k.grouping).padStart(10)}` ); } console.log("property usage shifted visible shift largest shift avg shift"); let totalUsage = 0; let totalShifted = 0; let totalVisible = 0; for (const cls of Object.keys(INVENTORY)) { const g = migrationCost(INVENTORY[cls], scales[cls], THRESHOLD[cls]); totalUsage += g.usage; totalShifted += g.shifted; totalVisible += g.visible; console.log( `${cls.padEnd(16)} ${String(g.usage).padStart(6)} ${String(g.shifted).padStart(8)} ` + `${String(g.visible).padStart(15)} ${String(g.largest).padStart(15)} ${g.avgShift.toFixed(2).padStart(10)}` ); } console.log( `${"total".padEnd(16)} ${String(totalUsage).padStart(6)} ${String(totalShifted).padStart(8)} ${String(totalVisible).padStart(15)}` + ` shifted rate ${((100 * totalShifted) / totalUsage).toFixed(1)}% visible rate ${((100 * totalVisible) / totalUsage).toFixed(1)}%\n` ); } // --- hybrid path: from-scratch scale + a logged exception --------------------- // A raw value that does not visibly shift onto the scale is snapped to it; a value that // does shift either gets a step added to the scale or is snapped anyway. The criterion for // adding a step is the usage count. const EXCEPTION_THRESHOLD = 3; console.log("--- hybrid path: from-scratch scale, decision for visible shifts ---"); console.log("property raw value usage target shift decision"); const added = {}; for (const cls of Object.keys(INVENTORY)) { added[cls] = []; for (const [d, n] of Object.entries(INVENTORY[cls])) { const value = Number(d); const target = nearest(value, FROM_SCRATCH[cls]); const shift = Math.abs(target - value); if (shift <= THRESHOLD[cls]) continue; const decision = n >= EXCEPTION_THRESHOLD ? "add a step to the scale" : "snap the value"; if (n >= EXCEPTION_THRESHOLD) added[cls].push(value); console.log( `${cls.padEnd(16)} ${String(value).padStart(9)} ${String(n).padStart(6)} ${String(target).padStart(6)} ` + `${String(shift).padStart(5)} ${decision}` ); } } console.log("\nproperty expanded scale steps indistinguishable visible remaining"); for (const cls of Object.keys(INVENTORY)) { const scale = [...new Set([...FROM_SCRATCH[cls], ...added[cls]])].sort((a, b) => a - b); const k = scaleQuality(scale, THRESHOLD[cls]); const g = migrationCost(INVENTORY[cls], scale, THRESHOLD[cls]); console.log( `${cls.padEnd(16)} ${scale.join(",").padEnd(32)} ${String(k.steps).padStart(5)} ` + `${String(k.indistinguishable).padStart(18)} ${String(g.visible).padStart(17)}` ); } // --- phased migration: coverage after a freeze --------------------------------- // Freeze: new declarations may come only from the scale. Old declarations pass only when // that file is touched for another task. r is the share of old declarations touched per period. console.log("\nperiod r=0.10 r=0.20 r=0.35 (share of declarations matching the scale)"); const initial = 0.6; // share of declarations already matching the scale at the moment of the freeze for (const period of [0, 1, 2, 3, 4, 6, 8]) { const row = [0.1, 0.2, 0.35].map((r) => (1 - (1 - initial) * Math.pow(1 - r, period)).toFixed(3)); console.log(`${String(period).padStart(6)} ${row[0].padStart(7)} ${row[1].padStart(7)} ${row[2].padStart(7)}`); }
--- A from scratch ---
property scale steps indistinguishable grouping
spacing 4,8,12,16,24,32,48,64,96 9 0 1
font size 13,16,20,25,31 5 0 0
corner radius 4,8 2 0 1
property usage shifted visible shift largest shift avg shift
spacing 45 18 4 4 0.98
font size 22 13 2 2 0.68
corner radius 14 6 1 4 0.79
total 81 37 7 shifted rate 45.7% visible rate 8.6%
--- B from existing ---
property scale steps indistinguishable grouping
spacing 6,8,10,12,14,16,24 7 5 0
font size 12,13,14,15,16 5 4 0
corner radius 3,4,6,8 4 3 0
property usage shifted visible shift largest shift avg shift
spacing 45 9 6 8 0.76
font size 22 4 4 8 0.91
corner radius 14 1 1 4 0.29
total 81 14 11 shifted rate 17.3% visible rate 13.6%
--- hybrid path: from-scratch scale, decision for visible shifts ---
property raw value usage target shift decision
spacing 20 3 16 4 add a step to the scale
spacing 28 1 24 4 snap the value
font size 18 1 16 2 snap the value
font size 22 1 20 2 snap the value
corner radius 12 1 8 4 snap the value
property expanded scale steps indistinguishable visible remaining
spacing 4,8,12,16,20,24,32,48,64,96 10 0 1
font size 13,16,20,25,31 5 0 2
corner radius 4,8 2 0 1
period r=0.10 r=0.20 r=0.35 (share of declarations matching the scale)
0 0.600 0.600 0.600
1 0.640 0.680 0.740
2 0.676 0.744 0.831
3 0.708 0.795 0.890
4 0.738 0.836 0.929
6 0.787 0.895 0.970
8 0.828 0.933 0.987
Shifted-Value Count Is the Wrong Metric
The two strategies’ total rows point the expected way at first glance: 45.7% of uses shift when built from scratch, only 17.3% when built from existing. It is not surprising that a scale summarizing the inventory sits closer to the inventory.
The next column reverses that reading. The share of uses that change visibly is 8.6% when built from scratch, 13.6% when built from existing. That is, the scale derived from the inventory shifts fewer values, but it shifts a larger share of them visibly.
The reason lies in the tail of the distribution. Building from existing selects the most-used values; because 80% of usage concentrates in small values, the scale gets squeezed between 6 and 24. Large values — 28- and 32-pixel section gaps — fall outside the scale and get pulled to 24; 20 pixels drops to 16 as well. Because small values shift almost not at all, the total shifted count looks low, but every shift that does happen is a large one: the largest shift is 8 pixels.
In the scale built from scratch, the situation is reversed. A large number of values shift 1–2 pixels — these fall below the distinguishability threshold and close at no visible cost. Only seven uses actually change.
The metric that follows is this: migration cost is measured not by the number of shifted values but by the number of shifts that exceed the threshold. This distinction is a consequence of the finding in the Repetition and Consistency lesson: most inconsistency is invisible, and invisible inconsistency is fixed at no cost.
A Summary of the Inventory Is Not a Scale
The scale quality tables show a second difference, and this difference is not open to debate.
In the three scales built from scratch, the number of neighbor pairs falling below the distinguishability threshold is zero. In the scales built from existing, that same count is 5, 4, and 3, respectively — 12 in total. The font-size scale runs 12, 13, 14, 15, 16 — none of the five steps can be told apart from the next. A scale like this does not reduce the decision count, it only documents the indecision that was already there.
The grouping column gives the same result a second time. The from-scratch spacing scale has one neighbor pair with a ratio of 2 or more; the from-existing scale has none. The rule in the Proximity and Grouping lesson required a group boundary to be at least twice the group’s inner spacing; with the from-existing scale, that rule cannot be expressed through neighboring steps.
The conclusion is this: the inventory’s most-used values do not form a scale. A scale’s job is to narrow options; selecting the most-used values legitimizes the most-often-made indecisive decisions.
The Hybrid Path Keeps the Scale, Logs the Exception
The third block builds the path between the two strategies. The scale is built from scratch; then every raw value that shifts visibly is handled one at a time, and one of two decisions is made.
The criterion is the usage count. In the Justifying the Need for a System lesson, the minimum usage count required for a decision to enter the system was computed as three; that same threshold is used here as the criterion for adding a step to the scale. A raw value used three or more times shows that the scale is missing something; a value used once or twice gets snapped onto the scale.
In the table, only one value crosses the threshold: the 20-pixel spacing, used three times. It is added to the scale, and the spacing scale becomes 4, 8, 12, 16, 20, 24, 32, 48, 64, 96. In the expanded scale, the number of indistinguishable neighbor pairs is still zero — the addition did not break the scale — and the remaining visible usage in spacing dropped from 4 to 1.
The other four values — the 28- and 12-pixel spacing, and the 18- and 22-pixel font size — get snapped onto the scale, each with a single use. These decisions go into the exception log from the Repetition and Consistency lesson: which value was snapped to what, and which screen was affected, is written down. If no log is kept, the same value comes back six months later and no one knows why it was removed.
Freezing Spreads the Migration Over Time
The final table measures how the migration is carried out. Changing every declaration at once is the fastest path and the riskiest one: the entire change ships at the same time, and a single mistake affects every screen at once.
The alternative is a freeze: after a set point in time, every newly written declaration must come from the scale; old declarations pass only when that file is touched for another task. If 60% of declarations already match the scale at the moment of the freeze, the coverage rate then rises depending on what share of old declarations gets touched each period.
The table compares three speeds. If 10% of old declarations are touched per period, coverage stays at 0.828 after eight periods; if 35% are touched, it reaches 0.987 in the same span. At the middle speed of 20%, it is 0.933 after eight periods.
Two conclusions follow. First, freezing is sufficient on its own, but far too slow for a slow-moving product; products with a low touch rate need a planned migration on top of it. Second, coverage never reaches a full 1 — exponential approach always leaves the last few percent behind. This remainder means the migration has to be finished by hand at some point.
Which Strategy, and When
The decision looks at three inputs. If the product is new or the existing base is small, building from scratch is the only reasonable path; there is no migration cost to begin with, and the scale quality is taken in full. If the existing base is large and tolerance for visible change is low — a contractually fixed appearance, a heavily used internal tool — the hybrid path is chosen: the scale is built from scratch, and a step is added for raw values whose usage crosses the threshold.
Building from existing, then, survives not as a scale-building strategy but as a reading of the inventory. The most-used values do not give the scale; they give the region where the scale should concentrate. The answer to why a scale built from scratch should be finer at the low end and coarser at the high end lies in that distribution.
Summary
- On the same inventory, building from scratch shifts 45.7% of uses but changes only 8.6% of them visibly; building from existing shifts 17.3% and changes 13.6% visibly.
- Migration cost is measured by the number of shifts exceeding the threshold, not by the number of shifted values; a scale derived from the inventory preserves small values and cuts off large ones.
- The inventory’s most-used values do not form a scale: across the three from-existing scales, the number of indistinguishable neighbor pairs is 12, and the number of pairs meeting the grouping ratio is zero.
- The hybrid path builds the scale from scratch and sorts visibly shifting raw values by usage count; a value used three or more times gets a step added to the scale, fewer uses get snapped onto it and logged as an exception.
- Freezing spreads the migration over time; coverage approaches exponentially and never reaches a full 1, so the remainder has to be closed with a planned effort.
- Building from scratch is chosen for a new or small-base product, the hybrid path for a large base closed to change; building from existing is not a scale strategy but a reading tool.
Next Step
Once the scales are built, the system’s numeric skeleton is ready, but the skeleton does not say what should look right. Two separate interfaces can be built with the same scale, and neither may carry the institution’s voice. What determines a decision that cannot be tied to a number is a separate question: whether a button is filled or outlined, whether an error message apologizes or instructs, whether the interface speaks quietly or encourages. The next lesson builds the foundation for those decisions; it computes, on a matrix, which conditions make a principle force a decision, which principle sits idle, and how a tone-of-voice contract gets checked.
To keep your progress and take notes, Log in
My notes
Log in to take notes.