Skip to content

Bug fixed in a component library

This is the pagination bug moved into a component library. What changes is who reads the docs and who depends on the code. In an application the help page describes behaviour to users. In a library the docs describe an API to developers, and a released version is code other people have already built against.

A component library that publishes a Pagination component. The consumer passes the page count as a totalPages prop.

Artifact-set Holds
{spec} Intent and criteria for the component’s behaviour
{code, test} The component and its tests, one unit of change
{API docs} The reference page: props, types, defaults, and a usage example
{mockups} The design-system mockups of the component
{published package} The released versions on the registry

The usage example on the reference page computes the page count for the consumer:

<Pagination totalPages={Math.floor(items.length / pageSize) + 1} />

Connections:

  • {spec} to {code, test}
  • {spec} to {API docs}
  • {spec} to {mockups}
  • {mockups} to {code, test}
  • {API docs} to {code, test}
  • {code, test} to {published package}
{spec}{API docs}{mockups}{code, test}{published package}

The {API docs} to {code, test} connection is the one the application example did not need. The spec states behaviour and does not name props. A spec that says the consumer supplies the page count is satisfied by totalPages, by pageCount, or by a function. The reference page must state which one, with its type and default, so it cannot be checked against the spec alone. It can only be checked against the code.

The spec’s criteria say the indicator shows the current page and the total, and the documented way to compute the total produces no empty page. The usage example breaks that criterion on an exact multiple, and nothing has noticed.

Workflow Input Owned Output Shape
Component delivery {spec} {code, test} none one link
Docs update {spec} {API docs} none one link
API reference {code, test} {API docs} none one link
Design update {spec} {mockups} none one link
Design implementation {mockups} {code, test} none one link
Release {code, test} none {published package} one link

API reference reads the code and owns the reference page. The page describes the code’s exported props, at the same level as the code, so it reads the code’s expression. A team could declare the opposite, a workflow that reads the page and owns the code, and use the page to plan an API before it is built. The declaration decides. Docs update also owns the page, for the usage example, which the spec constrains.

Neither declaration makes the page a contract. The page and the code can both be revised, so on that edge a change can lead from either side. The contract is {published package}. It is an output: a released version cannot be edited, only superseded. The release controller holds one convention: removing or renaming an exported prop is a breaking change, and needs a major version and a migration note.

The page has a standing specification of its own. Its use case is a consumer who arrives to learn what to pass the component. The prop names, types, and defaults are criteria bound to it. By default the leash stops any write that removes one, and an approver approves it. In this library the approver who holds the API is the maintainer.

The team keeps the default leash on every set. A break in the API stops twice by default: at the reference page, which loses a documented prop, and at release. Both stops ask the maintainer one question, whether the API may break. The team does not loosen {API docs} for every run to avoid that. The stop there is ceremony only when release asks the same question in the same run, and a loosening would also drop it in a run where release is not reached. Instead the maintainer answers once, and on accepting the break pre-approves the rest of the run’s gates.

A consumer reports an empty last page. The same intent reaches the library by two different routes, one per variant.

A developer decides consumers should not compute the page count at all. They replace totalPages with totalItems and pageSize, compute the count inside the component, round up, and commit. The diff touches the code and tests only. Every consumer passing totalPages breaks on upgrade.

  1. Lift. The change touches {code, test}. It is the source.
  2. Find candidates. Component delivery and design implementation own {code, test}, so they are upstream candidates. API reference and release read it, so they are downstream candidates.
  3. Distill. Each candidate reads the change within its own span.
    • Component delivery: every page shows at least one item. The new props are how the developer expressed it.
    • Design implementation finds nothing visual in the change and abstains.
    • API reference and release read the source as an input, so they read the change itself: totalPages is gone, and totalItems and pageSize are new.
  4. Ask the controllers above. Component delivery asks the {spec} controller. The intent implies that an exact multiple produces no empty page, and that the documented way to reach a page count produces none. The spec already states both, so it holds. Nothing above the code replays.
  5. Reconcile at the source. The code controller checks the developer’s change against the spec’s criteria. The component rounds up and an exact multiple produces no empty page, so it keeps the change as written. The report is a match. The controller also compares the result with the set’s standing specification. The example authors no use cases for {code, test}, so its standing specification is the criteria it stood under from {spec} and {mockups}. Neither names a prop, so keeping the change removes nothing.
  6. Propagate. The downstream candidates run from the reconciled code.
    • API reference documents totalItems and pageSize and removes totalPages. The usage example no longer computes a count. Removing a documented prop contradicts the page’s standing specification, so the leash stops the write for approval.
    • Release reads the code. Its controller finds an exported prop removed, which its convention names a breaking change, so the emission needs a major version and a migration note. It is an output write, so it waits for approval. The report is change course: the fix changed the component’s API.
    • Both stops reach the maintainer, the approver who holds the API, with one question.
  7. Decide. The maintainer answers once, at whichever stop reaches them first. The answer is a recorded decision.
    • Rejected. The decision adds a criterion, the component accepts totalPages, and creates the next criteria version. The code controller reconciles again and restores the prop. Neither stopped write goes ahead, and API reference finds the page already documents the restored prop. Docs update asks the spec, which holds, and the docs controller finds the usage example rounding down, which breaks the spec’s criterion, so it amends the example to round up. Release has nothing breaking left to emit. Each of these writes returns its set to its standing specification, and the formula is not a criterion of the page, so none of them stops.
    • Accepted. The decision widens the intent: consumers no longer compute the page count. The maintainer also pre-approves the rest of the run’s gates. The other stopped write proceeds if its criteria pass. The page documents the new props, and release emits a major version with the migration note its convention requires. The page is then a change of its own. Docs update owns it, so it distills under the run’s intent and asks the {spec} controller, which holds. The docs controller reconciles the new page against the spec: the documented way to reach a page count now produces no empty page. The old rounding-down example is gone with the prop. The formula was expression that the spec’s criterion checked, not a criterion of the page, so removing it removes nothing. Keeping the prop removal falls under the same pre-approval.

If the maintainer rejects the break:

  • {spec} is unchanged.
  • {code, test} accepts totalPages as before.
  • {API docs} documents totalPages, and its usage example rounds up.
  • {mockups} is unchanged.
  • {published package} has no breaking release.

If the maintainer accepts it, {code, test} and {API docs} carry the new props, and {published package} has a new major version with a migration note.

The break reaches a person before it reaches a consumer, and release holds it at the one set where it cannot be taken back. Either answer is recorded, so the settled state is chosen by a decision rather than by whichever workflow finished last.

The maintainer is asked once. The break stops at two sets, and the pre-approval given with the first answer carries the second write, which still has to pass its criteria. At release those criteria include the convention, so a release with no major version or no migration note does not proceed. The pre-approval does not cover the quality of the note. A maintainer who wants to read it before it ships does not pre-approve, and answers again at release.

An earlier version of this example declared an API contract workflow, reading the page and owning the code, and relied on it to stop the break. It never could. Docs update owns the page, and the spec does not name props, so a run that read the new props as part of the intent could document them, and the code would then match its own docs. A guard on an edge between two revisable sets holds only until someone revises the other side.

Four judgements carry the status.

  • The release controller’s convention. Without removing an exported prop is a breaking change, release still needs approval for the emission, but the approver sees a version, not a break. The guard is the convention plus the output, not the output alone.
  • Declaring release at all. With no workflow that reads the code and outputs the package, nothing in the run reaches the registry. That is a gap in the declarations, the same kind selection raises when a strained set has no writer. The API reference write would still stop and reach the maintainer, because the team kept the default leash on {API docs}. Had it loosened that set for every run, nothing would stop, and consumers would find the break.
  • Whether {code, test} has a standing specification of its own. The example gives it none beyond the criteria from above, so reconciliation at the source removes nothing. If the team authored the component’s tests through use cases, totalPages would be a criterion of the code, and the source would stop first, on the question release asks. The maintainer would answer there and pre-approve the rest. The model says a set’s criteria are authored through use cases, but not whether an implementation set carries use cases of its own or only the criteria of the sets above it.
  • Where the connection sits. {spec} connects to {code, test} at behaviour. {API docs} connects to it at the exported props and their types, a rung below. The model has not said how levels are identified across artifact types, and the direction of API reference rests on it. Here the rung is plain from the prop list, and in general it may not be.

A technical writer corrects the usage example to round up. The diff touches the reference page only.

  1. Lift. The change touches {API docs}. It is the source.
  2. Find candidates. Docs update and API reference own {API docs}, so both are upstream candidates. Nothing reads the page, so there are no downstream candidates.
  3. Distill. Docs update: the documented way to reach a page count produces no empty page. API reference finds nothing about the code’s exported props in the change and abstains.
  4. Ask the controllers above. Docs update asks the {spec} controller, which already states the rule, so it holds.
  5. Reconcile at the source. The docs controller keeps the correction as written. The report is a match. It compares the result with the page’s standing specification. The prop list is unchanged. The formula is expression that the spec’s criterion checks, not a criterion of the page, so correcting it removes nothing. The write proceeds under the default leash, and nobody is asked.
  6. Propagate. Nothing replays, and no other set changes.

The same as variant A when the maintainer rejects the break.

The correction lands where the defect lived, and a docs fix pulls in no code workflow. Variant A reaches the same state from the code side when the break is rejected, so one intent entering at two points ends in one place, and this pair meets confluence. When the break is accepted, the two variants end in different places because the decision changed the intent, and the record says so.

The status rests on the formula not being a criterion of the page. Had the team bound compute the count as Math.floor(items.length / pageSize) + 1 to the consumer’s use case, the correction would reverse it and stop for approval. That is the case where a fix to a stated rule and a change of direction look the same from inside the set. Here the rule is the spec’s criterion, and the formula is how the page failed it.

  • A connection between two implementations of one specification. {API docs} and {code, test} both implement {spec}, and the docs still need the code, because the spec leaves the API open. A connection between siblings cannot always be derived through their common parent.
  • Direction lives in the workflow, and level suggests it. The page describes the code at the code’s level, so API reference reads the code.
  • Contracts at outputs. The guard against the break is the release controller’s convention and the approval an emission needs, not a declaration on an edge between revisable sets.
  • The leash against a standing specification. Removing a documented prop contradicts the page, and it stops. So does release. The maintainer answers once and pre-approves the rest of the run, so one decision covers stops at two sets without loosening either set for every run.
  • Reconciling at the source. The developer’s change meets the spec’s criteria and is kept as written. What stops it is not the reconciliation but the output, because {code, test} states no criterion of its own that the change removes.
  • Distillation per workflow, with design implementation and, in variant B, API reference abstaining.
  • Candidates on both sides. Variant A runs API reference and release as downstream candidates.
  • Recorded decisions choosing between settled states. Rejecting the break converges with variant B; accepting it widens the intent.
  • Strain that predates the change. The docs example broke the spec long before either variant. Variant A removes it with the prop, or fixes it when the prop is restored. Without any change touching the component, only a criterion a controller tests on schedule would find it.