hamsa software
← All articles
Engineering7 min read

A dependency update that changes 0017 to 17

A dependency can install cleanly and return the wrong business meaning. A fictional normalisation change shows where an application contract test earns its place in routine upgrades.

Two rectangular shapes are joined by four short stitches, representing the application boundary across which an identifier must retain its meaning.

The changed file is not always where the risk lives

The upgrade installs, the checks pass, and customer reference 0017 comes out as 17. Only the manifest and lockfile may have changed, but the exported value now identifies something different to the business. In the fictional example below, the library still returns a string without complaint. I would follow that string to the application’s boundary before deciding whether a successful dependency update has preserved the job it was used for.

Routine updates are worth making routine. Letting them accumulate creates larger, harder-to-understand changes later. GitHub’s Dependabot documentation describes automated proposals for version updates, including review of tests and release information. [1] The automation can keep the work visible. It cannot decide whether a changed interpretation is acceptable to the particular business using the package.

The two library versions are invented to make that decision easy to inspect. One trims surrounding whitespace; the next interprets the result as an integer and turns it back into a string. Either can make sense for a numeric task. An account reference whose leading zeros matter asks for a different promise, so the useful question is whether the application chose the right interpretation before the package changed it.

Follow the value past the successful call

The input 0017 must remain 0017 under the teaching contract. Spaces around it may be trimmed, but the digit sequence itself is significant. Fictional v1 returns 0017; fictional v2 returns 17. No exception occurs. A test which only checks that the return type is string passes while the integration may now look up another record, fail a match or export a reference the recipient cannot recognise.

The case 17 is a useful control because both versions return 17. A dataset full of convenient references without leading zeros would make the upgrade appear harmless. The surrounding-space case is useful for another reason: it confirms which normalisation the application deliberately allows. A contract test should explain both what may change and what must survive, rather than treating every difference as either automatically bad or automatically a bug fix.

The downloadable application adapter trims allowed outer whitespace, requires the chosen ASCII digit format and preserves the remaining string. It rejects 17x rather than extracting its convenient numeric prefix. Those two choices belong together: spaces at this boundary may be incidental, while the identifier’s digits remain significant. An adapter that simply accepted everything unchanged would preserve zeros but withdraw the input rule. This one states which normalisation the application owns; a library whose job is to normalise quantities numerically has a different contract.

The upgrade may be exposing the application’s earlier mistake rather than breaking the library’s own promise. A numeric helper can quite reasonably return 17 for something the application should have kept as text. An adapter that states the permitted trimming and preserves the remaining identifier is then a more durable repair than refusing every future update. It moves the important rule into code the application owns, where a later maintainer can see why those zeros must survive.

The dependency succeeded at the wrong interpretation

Two fictional library implementations normalise text differently. The application treats account references as identifiers, so leading zeros must survive. These are not real package versions or a reported regression.

Scroll sideways to see all columns.

Raw inputRequired identifierFictional v1Fictional v2 /adapter
00170017001717 / 0017
␠0017␠0017001717 / 0017
17171717 / 17
17xReject under contractNot a supported caseAdapter rejects
The downloadable original Python example checks the identifier contract and adapter. Spaces are shown as ␠ for visibility. The application adapter preserves an identifier; a numeric quantity may need a different normalisation rule.

A version number is a signal about a declared interface

Semantic Versioning ties major, minor and patch changes to compatibility with a declared public API. [2] It is useful information, not an executed proof of the application’s outcome. A library can change an undocumented behaviour, fix something it considered a bug or contain an unintended regression. The reviewer still needs to understand which behaviour the application relies on and how the candidate affects it.

Read the release notes in that context. A note about numeric parsing is highly relevant to the fictional account adapter; a new optional display theme may not be. Inspect migration guidance and configuration defaults where they touch the application’s use. The task is not to memorise every change in a large dependency. It is to trace the subset that crosses the application’s important boundaries.

Record the resolved dependency set used for the check. npm describes package-lock.json as recording the exact generated tree for reproducible installs and reviewable tree changes. [3] That helps establish what was tested. It does not prove the tree is safe, nor that every environment uses it correctly. A manifest range alone may describe several possible resolutions; the lockfile gives the comparison a concrete dependency set.

Keep the update proposal, CI result and release decision distinct. A bot proposes a candidate dependency set, and CI reports the checks configured for that run. The release judgement must account for the relevant application boundaries as well. For 0017, evidence that the candidate installs and returns a string does not establish that the original reference survives the conversion or later export.

Put the contract case at the boundary you own

For the identifier example, the important test sits at the application adapter or exported record boundary. Given the original reference 0017, the downstream result should still identify 0017. Testing only the fictional library’s documented numeric conversion could legitimately expect 17 and miss the application mismatch. The layer being tested determines which promise the assertion can protect.

Use specimens that disagree when the interpretation changes: 0017, an allowed space around it, 17x and the ordinary 17 control. A larger collection of convenient numbers would add little to that question. Put the expected result beside each case, particularly if the project uses snapshots. A reviewer asked to refresh a changed output should be able to see that accepting 17 would change an account reference, rather than merely acknowledge a harmless library formatting difference.

Inspect downstream output as well when formatting or serialisation can change meaning. A correct string in memory can become a number in a CSV-reading tool or another application. The upgrade might expose an existing weakness rather than create the entire problem. Trace enough of the receiving contract to know what has been preserved, and avoid claiming a local adapter test proves every subsequent system will treat it correctly.

The same approach applies to dates, rounding, encoding, sorting and missing values, but each needs its own meaningful boundary case. A generic test asserting that the service starts would not reveal any of them. Choose the few contract behaviours whose loss would create a costly or hard-to-detect outcome for this application. Those tests remain useful across many upgrades because they state what the business requires, rather than mirror a package’s implementation.

Keep the maintenance decision visible

A small team needs an owner and a cadence for reviewing update proposals, not an indefinitely growing inbox of untouched bot requests. Separate urgent security work from routine compatibility work using the actual advisory and exposure evidence. Where a change cannot yet be adopted, record the reason, the affected use and the condition for revisiting it.

Group changes where that makes their interaction understandable, and isolate a troublesome dependency when a broad batch would obscure the cause. The right grouping depends on the project. Updating twenty unrelated packages together can make a failure difficult to attribute; requiring a ceremony for every harmless patch can make the process unsustainable. The aim is a reviewable unit with enough evidence to decide and recover.

Keep the tested dependency tree, relevant release note and boundary result with the adapter decision. If an unexpected export appears later, those give the team concrete versions to compare. Recovery also needs to follow the data already written. Both 0017 and 017 can collapse to stored 17; restoring the old library cannot tell which original belonged to a particular row. Retained raw references, where justified, may support a reviewed correction. Changing the manifest until the build goes green would restore a future behaviour while leaving that earlier loss untouched. The code and data therefore need related but separate recovery decisions, with enough of the original input to justify any correction.

For the next upgrade, the useful inheritance is small: a known dependency tree and application cases that explain why the leading zero and allowed whitespace matter. The update bot can keep proposals coming while those cases protect the interpretation the business relies on. If lossy output has already been stored, it still needs its own correction from the original references. That is the part a dependency rollback cannot do, however cleanly the previous version installs.

Rolling back code cannot restore discarded digits

The fictional conversion can collapse different source identifiers into the same stored output.

Follow the labelled arrows. On a narrow screen, scroll sideways if needed.

Rolling back code cannot restore discarded digitsDifferent source references: 0017 and 017. These fictional inputs are distinct identifiers before lossy numeric conversion. Both stored as 17. From stored 17 alone, the original reference and its leading zeros cannot be inferred. Output alone cannot restore the digits. A restored string-preserving adapter protects future input, not earlier rows. Retained raw source references may support a reviewed correction. From Different source references: 0017 and 017 to Both stored as 17: Lossy numeric conversion. From Both stored as 17 to Output alone cannot restore the digits: The original distinction is gone.Lossy numericconversionThe originaldistinction isgoneDifferent sourcereferences: 0017 and017These fictional inputs aredistinct identifiers beforelossy numeric conversion.Both stored as 17From stored 17 alone, theoriginal reference and itsleading zeros cannot beinferred.Output alone cannotrestore the digitsA restored string-preservingadapter protects futureinput, not earlier rows.Retained raw sourcereferences may support areviewed correction.
The library versions are fictional. This explains an information-loss limit, not an allegation about a real dependency.

Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.

See the Payment Operations Pilot

References & further reading

Primary sources behind this article. Living documentation may change.

  1. GitHub: Dependabot version updates and review workflow
  2. Semantic Versioning 2.0.0: declared API and compatibility signals
  3. npm: package-lock.json and the resolved dependency tree
Hamsa Jama

Practical engineering. A human perspective.

More about Hamsa Software and the person behind it. Editorial notes.

A few more things
worth a read.

Back to the reading room
Engineering7 min read

What does £240 belong to?

A readable table preserves relationships between values, row labels, column headings and actions across different ways of reading.

Engineering7 min read

When two calculations agree on the wrong answer

A shadow comparison exposes differences. Independent rule examples explain whether those differences are bugs, corrections or changed assumptions.