Borrowed Code · Nº 27

Borrowed Code

Open source & the dependency supply chain

A typical production application is a few percent code you wrote riding on a tree of packages written by strangers, maintained by volunteers, and pulled in by transitive resolution you never saw. This course is about treating that arrangement as what it is — a portfolio of trust relationships with legal, stability, and security terms — and managing it like you'd manage any portfolio: deliberately.

Module 01 Your app is mostly other people's code

Start with an audit rather than a definition. The wine-cellar product — a React front end and a Flask back end, roughly the size of something two engineers ship in a year — declares fourteen dependencies across its two manifests. Fourteen names, each one a deliberate choice somebody argued for in a pull request. Installing those fourteen names produces 1,161 packages on disk. The application ships around 2.1 million lines of third-party code alongside the 28,000 lines the team actually wrote: about 1.3% of what runs in production came from anyone on the payroll.

That ratio is not evidence of laziness. It is the industry norm, and it is the reason the product exists at all — no two-person team writes a rendering engine, a WSGI server, a TLS stack, and a date library and still ships a feature. But a ratio that lopsided changes what your job is. You are not primarily an author of software; you are primarily an assembler and a curator of it, and the discipline that job requires is the subject of this course.

The uncomfortable count

The wine-cellar audit is the opening move of this guide and it recurs in every module, so the numbers are worth fixing in memory. Fourteen direct dependencies are declared: on the front end, React, a charting library, a form helper, a router, an HTTP client, a date formatter, and a test runner; on the back end, Flask, an ORM, a migrations tool, a validation library, a task queue client, a PDF generator, and a WSGI server. That is the list a reviewer sees in a pull request. It is also, in practice, the only list anybody ever reads.

Running the install produces 1,161 packages. The extra 1,147 are transitive dependencies: packages nobody on the team named, evaluated, or approved, present because something the team did approve required them. Measured by lines, the shipped artifact is roughly 2.1 million lines of third-party source against 28,000 lines of application code. If you strip the test runner and dev-only tooling out of the count the ratio improves — to about 1.9 million against 28,000. It does not improve enough to change any conclusion in this course.

The dependency iceberg: fourteen declared dependencies above the waterline, 1,147 transitive packages below it, and a proportion bar showing 28,000 lines written against 2.1 million lines shippedAbove the waterline — what the pull request showsreactchart-kitrouterflaskorm+ 9 more14 declared · chosen by you · argued for in reviewwaterline: the limit of what anyone reviewsBelow — what the install actually placed on disk1,147 transitive packages · chosen by your dependencies' maintainers, recursively1,161 installed in total · reviewed by no one on your teamCode you wrote vs. code you ship≈ 2.1M lines shipped — 98.7% written by strangers▏ ≈ 28k lines written by your team — 1.3%
Figure 1.1 — The dependency iceberg. Fourteen dependencies are declared and reviewed; installing them places 1,161 packages on disk, so 1,147 arrived by transitive resolution nobody on the team evaluated. Measured in lines, about 1.3% of what the wine-cellar app ships was written by the people responsible for it — the master picture the rest of this course refers back to.
The load-bearing idea

The declared list and the installed list are different objects, and they differ by two orders of magnitude. Every claim you make about your software — its quality, its license posture, its attack surface — is a claim about the installed list, but almost every process you have inspects the declared one.

Direct, transitive, and who chose them

Precision here pays off for the rest of the course. A direct dependency is one you named in your manifestpackage.json, pyproject.toml, requirements.txt. A transitive dependency is one that arrived because a direct dependency (or one of its dependencies, recursively) required it. The distinction is about authorship of the decision, not about importance or proximity to your code: a transitive package four levels down can execute on every request, hold your database credentials in memory, and still have entered your system because a maintainer you've never emailed added a line to their own manifest one Sunday.

The dependency tree is the resolved graph an install produces from your manifest plus the registry's current contents. It is a tree in the loose sense — really a directed graph, since the same package appears in many branches — and its shape is not yours to control. You control the roots. Everything else is the transitive closure of other people's decisions.

Flowchart: two manifests feed a resolver, which expands fourteen direct dependencies into a multi-level tree of transitive packagespackage.json7 directpyproject.toml7 directchosen by youresolver+ registry statedirect · level 1direct · level 1direct · level 1level 2level 2level 3level 4… 1,147 totalchosen by strangers, recursivelyYour authority ends at level 1. Everything past it is the transitive closure of decisionsmade by maintainers you have never evaluated — and it all runs with your privileges.
Figure 1.2 — From manifest to tree. Two manifests declare fourteen names; the resolver combines those constraints with the registry's current state and expands them into a multi-level graph. Only the accent-stroked level-1 nodes were chosen by your team; every level below is inherited, and inherited code runs with exactly the same privileges as code you wrote.
From your GRC work

This is fourth-party risk, the problem vendor management has always had: you diligence your vendor, your vendor subcontracts, and the subcontractor's subcontractor handles your data. Software just makes the chain longer, automates the subcontracting, and skips the paperwork. The instinct you already have — who else is in this arrangement, and what did they agree to? — is the right one; the rest of this course gives it tooling.

Trust is transitive; review is not

Installing a package grants it two kinds of execution: at install time, when many ecosystems let a package run arbitrary setup code on the machine doing the install, and at run time, inside your process, with your file handles, your environment variables, and your database credentials. There is no sandbox between a transitive dependency and your application secrets. Trust therefore flows to the entire closure automatically — the tree's leaves are as trusted as its roots, because the runtime cannot tell them apart.

Review does not flow that way. Suppose your team is exceptionally disciplined and reviews third-party code at 500 lines an hour, which is roughly double what careful reviewers actually sustain. Two million lines is 4,000 hours — two engineer-years — for one snapshot of the tree, which is obsolete the next time anything updates. The asymmetry is total: trust propagates for free and review does not scale at all.

Everything else in this course exists because of that gap. You cannot read the code, so you substitute other evidence: who maintains it and whether they can keep going (m2), what terms it arrived under (m3), what the version number promised (m4), what you actually got (m5), how the tree is attacked (m6), whether to enter the relationship at all (m7), and how to keep it healthy and exit it under pressure (m8 and m9).

When it misleads

"It's open source, so many eyes have reviewed it" is the most comfortable false premise in this field. Many eyes have reviewed React. Nobody has reviewed the flatten-nested-config helper sitting four levels down with 900 weekly downloads — and "many eyes" was exactly the assumption the xz backdoor exploited, since the malicious payload was not in the readable source tree at all. Popularity concentrates attention at the top of the tree; the risk lives at the bottom.

What "adding a dependency" actually signs

The lawyer's reframe is the useful one: an install is not a code change, it is an execution of terms. Four commitments attach at once, and each has a module in this course.

You sign legal terms — the license is the only thing making your use lawful, and its conditions bind whether or not you read them (m3). You sign a stability contract — a version number is a stranger's promise about what will and won't break, and a version range is a standing order to accept future releases sight unseen (m4, m5). You sign an attack-surface expansion — every package in the closure is a place an adversary can aim, and they do (m6). And you sign an ongoing maintenance obligation — updates to take, vulnerabilities to triage, and eventually an abandonment or a migration to absorb (m7 through m9).

None of those commitments appear in the diff. The diff shows one added line. That mismatch — a one-line change that binds you on four dimensions indefinitely — is the reason this course exists, and it is why the closing artifact in m9 is a policy rather than a tool.

The four signatures, concretely

The wine-cellar team added chart-kit in a two-line pull request. That single line brought in 87 packages, one of which carries a GPL-2.0 license the team will discover in m3; a caret range that will accept a patch release breaking date parsing in m4; an install script running unrestricted on developer laptops in m6; and a maintainer whose release cadence is about to fall off a cliff in m2. Every one of those consequences was signed at merge time; every one surfaced months later.

Module 02 Who actually maintains this

Module 1 counted the tree. This one looks at who is holding it up. A package is not a finished artifact sitting on a shelf; it is the current output of a production process run by people, and the properties you care about — that bugs get fixed, that security reports get answered, that a release ships when a runtime changes underneath it — are properties of that process, not of the code sitting in your node_modules today.

The uncomfortable finding, once you look, is how thin the process usually is. A large fraction of the infrastructure the software industry runs on is maintained by one person, unpaid, in the hours after their actual job. That is not a scandal to be indignant about; it is a structural fact to plan around, and it is the precondition for several of the incidents in m6 and m9.

Where packages come from

Between a bug existing and a fix arriving in your build sits a queue and a person. Someone must triage the issue, decide it is real, review or write a patch, judge whether it breaks anyone downstream, cut a release, and publish it. Each of those steps consumes attention from a specific human being. When you ask "is this package well maintained?" the answerable version of the question is: how much attention does this process receive, and from how many people?

The vocabulary matters because the roles differ in authority. A contributor is anyone whose changes get accepted — contribution confers no standing. A maintainer holds commit and release rights: the ability to merge and, critically, to publish to the registry under the package's name. Registry publish rights are the crown jewels of the ecosystem, because the artifact you install is whatever the publisher pushed, not whatever is in the public repository. Module 6 turns on exactly that gap.

Note

"Maintained by a company" usually means employees do the work on company time under company priorities. It does not mean a support contract exists, that anyone owes you a response, or that the license grants you anything beyond what the same license grants a hobbyist. Nearly every open-source license disclaims warranty entirely — you are using it as-is, from a company or from a stranger alike.

Governance models and what they buy you

Four models cover most of what you will meet, and each trades continuity against control differently. A solo maintainer gives you fast, coherent decisions and a bus factor of one. A small team spreads the load but often shares a single social circle and a single burnout cycle. A company-backed project gives you paid attention and a roadmap serving the company's interests, which can change — corporate backing protects against neglect, not against strategy. A foundation-hosted project (Apache, Linux Foundation, Python's PSF, or a project-specific body like Pallets) buys governance continuity, trademark and release process, and a decision path that survives any individual leaving — at the price of slower change.

You can classify a package in about five minutes from public signals: the number of distinct people who have published releases in the last two years, whether the repository lives under an organization or a personal account, whether there is a documented governance or release process, and whether the recent commit history is one name or many. That is the same evidence-gathering exercise as vendor viability review, run against a repository instead of a data room.

Quadrant chart placing solo, small-team, company-backed, and foundation governance models by continuity risk against decision speed, with wine-cellar dependencies plottedDecision speed and responsiveness →Continuity risk →Solo maintainerbus factor 1 · fastestSmall teamshared burnout cycleCompany-backedstrategy risk, not neglectFoundation-hostedsurvives any one exitchart-kitflask (Pallets)react (Meta)Placement is by governance model, not by code quality — a solo-maintained package can be the best code in your tree.
Figure 2.1 — The governance spectrum. Continuity risk and decision speed move together: the solo maintainer answers your issue tonight and may vanish next year, while a foundation project survives any individual departure and moves slowly by design. Wine-cellar's dependencies sit across the whole range — Flask under the Pallets project, React under Meta, and chart-kit under one person with a day job.

The sustainability gap

Here is the structural problem. Adoption of a useful library compounds: every project that depends on it becomes a dependency of other projects, so downstream usage grows super-linearly. Maintainer capacity does not compound. It is one person's evenings, and it is flat at best — it declines as life happens. Between a curve and a flat line, the gap widens forever.

What that gap feels like from inside is a stream of unpaid obligation from strangers: bug reports written as demands, security researchers with disclosure deadlines, corporate users requesting features for products the maintainer has never used, and no revenue anywhere in the arrangement. The predictable outcomes are burnout and exit, and the historical record is unambiguous. In 2016 a maintainer unpublished left-pad — eleven lines of code — during a naming dispute and broke builds across the ecosystem. In 2018 event-stream's exhausted maintainer handed publish rights to a volunteer who promptly shipped a cryptocurrency-stealing payload. In 2022 the author of colors and faker deliberately sabotaged his own widely-used packages — colors with an infinite loop, faker by deleting its code. In 2024, the years-long social-engineering campaign that produced the xz backdoor worked precisely by manufacturing pressure on a burned-out solo maintainer until he accepted help.

Curve chart: downstream usage rising steeply over six years against a flat single-maintainer capacity line, with the widening gap shadedYears since first release →Demand on the maintainer →yr 1yr 3yr 5yr 7dependents,issues, CVE reportscapacity: one person, evenings — flatyour risk accumulates herethe gap is the burnoutleft-pad · event-stream · colors/faker · xz — all downstream of this shape, not of bad people.
Figure 2.2 — The sustainability gap. Downstream usage compounds while a volunteer maintainer's capacity stays flat, so the gap between what a project owes its users and what its maintainer can supply widens with every year of success. The shaded region is where unanswered issues, unreviewed patches, and eventually burnout, handover, or sabotage accumulate — and where your risk lives.
The load-bearing idea

Popularity is a load, not a guarantee. The more critical a volunteer-maintained package becomes to the industry, the more pressure it puts on a person who is not being paid, has no obligation to you, and can stop at any time without notice. Every incident in this course's second half descends from that arithmetic.

Reading the downstream warning signs

None of these failures arrive without warning; they arrive without warning to people who were not looking. The signals are public and cheap to check. Release cadence stretching from monthly to quarterly to nothing. Issues open for a year with no maintainer reply, while new ones keep arriving. A README that has acquired a "looking for maintainers" or "this project is in maintenance mode" line. Commit history collapsing to a single name, or to no names for eighteen months. Funding notices with no funding. And the loudest signal of all — a maintainer writing publicly about being tired, which the industry has learned, repeatedly, to read as background noise rather than as notice.

Two of those deserve a distinction. Abandonment is the passive failure: the package stops receiving fixes and quietly rots against the world changing around it. Maintainer revolt is the active one: an unpublish, a protest release, a deliberate sabotage. Abandonment gives you months of warning and a slow-burn problem; revolt gives you none and lands as an outage. Your defenses differ accordingly — abandonment is answered by adoption discipline and migration planning (m7), revolt by pinning and lockfiles that stop today's registry from changing yesterday's build (m4, m5).

From your GRC work

You have run this analysis before under a different name: vendor viability review. Concentration risk, key-person dependency, financial runway, succession planning — the questions map almost one-to-one onto bus factor, governance model, funding, and documented handover. The difference is that a vendor signs an agreement obliging them to tell you things, and a maintainer signs nothing at all. You get the same analysis with worse disclosure, which means you must go get the signals yourself.

The anti-pattern

No plan for the day a critical dependency is abandoned. Symptom: asked what you would do if a load-bearing package stopped receiving fixes tomorrow, the honest answer is a shrug, and nobody can name who would own the migration. Corrective: for each dependency in the load-bearing quadrant of m7's Figure 7.2, write one sentence naming the successor or the exit path. One sentence. The value is not the plan's detail — it is discovering, calmly, which dependencies have no plausible answer.

Module 03 Licenses as a decision, not a footnote

This module teaches license classification and trigger analysis as engineering concepts. It is not legal advice, and the distinction is not a disclaimer ritual — it is the actual shape of the skill. What you need is the ability to look at a dependency tree and say "these are permissive, this one is copyleft, here is the event that would trigger its conditions, and here is the person whose job it is to decide what we do about that." Getting to that sentence quickly, and before an acquirer's counsel gets there first, is the whole competency.

You have an advantage here that most engineers lack. A license is a document that grants rights subject to conditions, and reasoning about what conditions attach on what facts is issue-spotting — the thing you did for eight years. The vocabulary is unfamiliar; the analysis is not.

The license is the whole permission

Start from the default rule, because everything else is an exception to it. Source code is a work of authorship, and copyright attaches automatically on creation. The default is therefore no: absent permission, you may not copy it, modify it, or distribute it. Publishing code to a public repository does not change that — visibility is not a grant. A repository with no license file is not "free to use"; it is all rights reserved with the source visible.

An open-source license is a unilateral grant of those rights, subject to conditions. That framing is the useful one: free code is precisely code whose price is stated as conditions rather than dollars. The conditions are cheap in most cases and non-trivial in a few, but they are never zero — even MIT, the minimal case, requires you to carry the copyright notice and license text with the distributed work.

From your litigation background

Open-source licenses are generally drafted as conditional grants rather than as bilateral contracts, and the drafters chose that structure deliberately. Acting outside the conditions means acting outside the scope of the grant — which frames the failure as infringement rather than as breach, and infringement brings a different set of remedies, including injunctive ones. Whether that framing holds turns on the license, the jurisdiction, and the facts; the point here is conceptual, not advisory. What it changes for you as an engineer is the seriousness of "we ignored the attribution requirement": it is not a paperwork lapse to clean up later.

The load-bearing idea

You are not deciding whether to accept the license — you accepted it at install. You are deciding whether the conditions it attaches are ones you can live with, and that decision is only cheap while you are still choosing. Its cost rises monotonically with every month the dependency stays embedded.

Permissive vs. copyleft

Two families cover the great majority of what you will meet. Permissive licenses — MIT, the BSD family, Apache-2.0 — grant broad rights and ask essentially one thing in return: preserve the copyright and license notices when you distribute. Apache-2.0 adds two things worth knowing: an express patent grant from contributors to users, and a patent-retaliation clause that terminates that grant if you sue over the software. That express grant is why Apache-2.0 is often preferred for anything commercially serious — MIT's patent position is left to implication.

Copyleft licenses — the GPL family — grant the same broad rights and add reciprocity: if you convey a work based on the licensed code, the recipient must get the same freedoms, which in practice means conveying under the same license, with source. GPL-2.0 and GPL-3.0 are strong copyleft, applying to the whole derivative work. LGPL is weaker, scoping the reciprocity roughly at the library boundary so an application can link to an LGPL library without the application itself becoming subject. MPL-2.0 is weaker still and file-scoped: modifications to MPL-licensed files must be shared, and the rest of your codebase is untouched. AGPL is the strongest, and gets its own treatment in the next section.

"Viral" is the shorthand everyone uses and it is a genuinely bad description. It suggests contagion through proximity — that GPL code in your node_modules somehow infects your repository by being nearby. That is not how it works. The obligation attaches to a specific act you take with the code, and if you never take that act, nothing attaches. Which is why the next section is the one that matters.

LicenseFamilyCore conditionPatent grantScope of reciprocity
MIT / BSDPermissivePreserve noticesImplied at bestNone
Apache-2.0PermissivePreserve notices, state changesExpress, with retaliation clauseNone
MPL-2.0Weak copyleftShare modificationsExpressPer file
LGPLWeak copyleftShare library modifications; allow relinkingExpress (v3)The library boundary
GPL-2.0 / 3.0Strong copyleftConvey source under same termsExpress (v3)The whole conveyed derivative work
AGPL-3.0Network copyleftSame, plus source to remote usersExpressWhole work, triggered by network use
The anti-pattern

Treating permissive as obligation-free. Symptom: your app ships with no third-party notices file, and nobody can produce one on request. MIT's single condition is attribution, and dropping it is a straightforward failure to meet the only condition attached to the grant — a bad look in a due-diligence review, and an easy one to avoid. Corrective: generate the notices file from the lockfile at build time (m5, m8) so it is always current and nobody has to remember.

What actually triggers the obligation

Copyleft conditions attach on conveyance — distributing the work to someone else — not on use. Running GPL software internally, modifying it for your own use, and never shipping it to anyone triggers nothing under GPL-2.0 or GPL-3.0. This is not a loophole; it is the express design of those licenses, and it is why so much internal enterprise tooling sits comfortably on GPL foundations.

Server-side use has historically fallen on the safe side of that line, because your users receive HTTP responses, not software. That gap — the "ASP loophole" — is precisely what AGPL was written to close: its section 13 extends the obligation to users interacting with the software over a network. If you run a modified AGPL component in your SaaS backend, the obligation to offer corresponding source can attach even though you never distributed a binary to anyone. "We don't distribute, so we're fine" is correct reasoning applied to the wrong license.

LGPL adds a different axis: the obligation is scoped around the library, with conditions concerning the user's ability to relink against a modified version. Static versus dynamic linking, and what your build actually produces, become facts that matter. The lesson is not to memorize the outcome for every combination — it is to recognize that these are fact-dependent questions, and that the facts are ones only you know: what you built, what you shipped, to whom, and in what form.

Decision flowchart showing which copyleft obligations attach at internal use, network service, distribution, and linking boundariesCopyleft code in treeUsed only internally?nothing leaves the buildingyesNo GPL obligationuse is not conveyancenoOffered over a network?SaaS, API, hosted appyesAGPL: obligation attachessource offered to remote usersGPL/LGPL: still not triggerednoConveyed to others?binary, app, applianceyesGPL: same terms + sourcescope = the conveyed workLGPL only:scoped at the librarylinking mode mattersThe gates are events, not proximity. Which gate you pass through is a fact about what you built and shipped —which is why this is issue-spotting, and why the answer is different for the same package at two companies.
Figure 3.1 — What triggers the obligation. Copyleft conditions attach at events, not by proximity: internal use triggers nothing, network service triggers AGPL alone, and conveyance triggers the GPL family — with LGPL scoping the result at the library boundary where linking mode becomes a relevant fact. The same package produces different obligations at two companies because they pass through different gates.

The AI wrinkle: code with no paper trail

Every mechanism above depends on knowing where code came from. A dependency arrives with a name, a version, a registry record, and a license file — imperfect provenance, but provenance. A suggestion from a coding assistant arrives with none of that. It appears in your editor as text, and the license, authorship, and origin metadata that would have travelled with it do not exist in the output.

The concern is not that AI output is generally infringing; for most short, functional, obvious code it plainly is not, and that is the vast majority of what these tools produce. The concern is the tail: a distinctive forty-line implementation, an unusual algorithm, a recognizable chunk of a known project, reproduced closely enough that its origin would be identifiable to anyone who happened to know the source — with the license header, the one artifact that would have told you the terms, stripped away by construction.

The workable rule is to treat AI-suggested code as an unvetted third-party contribution, because functionally that is exactly what it is. Nontrivial or distinctive blocks get a provenance check before merge. Keep contributions small and rewritten rather than pasted at length. Prefer generating code that calls your interfaces over code that reimplements a known library wholesale. And record the decision — a line in the PR noting that a block was AI-assisted and reviewed is cheap now and is the only evidence available later.

Cross-reference — Guide Nº 08

Guide Nº 08 (verifying AI-generated code) builds the review discipline for correctness: you own what you merge, and the reviewer's job does not shrink because the author was a model. Provenance is the same discipline pointed at a different property. The same review pass that asks "do I understand what this does?" should ask "do I know where this came from?" — and for a distinctive block, the honest answer is often no.

The anti-pattern

Shipping AI-suggested code with no provenance position at all. Symptom: asked in due diligence how the team handles license risk in assistant-generated code, nobody has an answer, and the git history cannot distinguish AI-assisted commits from any other. Corrective: one paragraph of written policy plus a PR checkbox. The paragraph does not have to be sophisticated — it has to exist, be followed, and be produceable on request.

The wine-cellar license audit

Run the audit and see what falls out. Across 1,161 installed packages the distribution is unremarkable: roughly 71% MIT, 18% Apache-2.0, 6% BSD variants, 3% ISC and other permissive, and the remainder a handful of oddities — including two packages whose license field is empty and one whose LICENSE file disagrees with its manifest metadata. Those three go on a list to resolve, because "unknown" is a worse answer than "GPL": an unresolved license is an obligation of indeterminate size.

The finding that matters is one package, four levels down, under chart-kit: a small polygon-simplification utility licensed GPL-2.0. Nobody chose it. It is not in any manifest the team wrote. It is compiled into the front-end bundle that ships to every browser that loads the app — and shipping a bundle to a browser is a much better candidate for conveyance than running a server is. That is the fact pattern, and it is precisely the one an acquirer's counsel finds in week two of diligence if you have not found it first.

License obligation ladder from attribution-only to network copyleft, beside the wine-cellar dependency tree with one transitive GPL-2.0 node and one provenance-unknown AI snippet flaggedWhat the license obligates →MIT · BSD · ISCpreserve notices — attribution onlyApache-2.0notices, state changes, express patent grantMPL-2.0weak copyleft, scoped per fileLGPLweak copyleft, scoped at the library boundaryGPL-2.0 · GPL-3.0strong copyleft on conveyance — same terms + sourceAGPL-3.0copyleft triggered by network useThe wine-cellar tree, overlaidthe bundle you shipserved to every browserchart-kit · MIT · directgeo-helpers · MIT · depth 3polygon-simplify · depth 4GPL-2.0 — chosen by nobodythe obligationtravels upwardto whateveryou conveyAI-suggested date parserlicense: ? — unmappedAn unmapped node is worse than a copyleft one: a known obligation can be priced, replaced, or complied with.An unknown obligation cannot even be accepted, because nobody can say what was accepted. (Nº 08 — AI provenance.)
Figure 3.2 — License obligation map. Licenses arrange on a ladder by what they demand, from attribution alone up to copyleft triggered by network use. Overlay a real tree and the finding is structural: a GPL-2.0 utility four levels beneath a permissively-licensed direct dependency, whose obligation runs upward toward whatever you actually convey — and beside it the AI-suggested snippet, which sits nowhere on the ladder because its terms are unknown.

The options form a menu with honest costs. Replace the transitive dependency, usually by upgrading or replacing chart-kit itself — cleanest, and here it means a two-week migration. Isolate it so the GPL code runs somewhere conveyance is not in question, such as server-side rendering — sometimes elegant, sometimes architecture theater. Comply — offer the corresponding source under the same terms, which is a real and legitimate choice that some companies make deliberately and which needs a decision from someone with authority to make it. Or rewrite the specific function; the utility in question is about ninety lines of well-documented geometry, and the team's own estimate was two days. The right answer here was replace-by-upgrade, because chart-kit was already on the m7 chopping block for unrelated reasons.

The AI-suggested snippet surfaced in the same audit: a forty-line date-window parser in the cellar-history endpoint, contributed six months earlier, that no one could source. It was not obviously copied from anywhere. It was also not obviously not. The team rewrote it against their own test cases in an afternoon and noted the reason in the commit message — cheaper than continuing to wonder, and it produced a written artifact where previously there was a shrug.

The audit, as a table

Clean: 1,155 packages under MIT / Apache-2.0 / BSD / ISC — action: generate a notices file from the lockfile at build time, since attribution is a live condition and nobody was meeting it. Unknown (3): two empty license fields, one manifest/file conflict — action: resolve upstream or remove; unknown is not a resting state. GPL-2.0 (1): transitive polygon utility under chart-kit, shipped in the browser bundle — action: replace via the chart-kit migration already planned, with the ninety-line rewrite as the fallback if migration slips. Provenance-unknown (1): the AI-suggested date-window parser — action: rewritten and documented.

The anti-pattern

Discovering your license posture during someone else's diligence. Symptom: the first complete inventory of your tree's licenses is produced by an acquirer's counsel, on their schedule, with your deal timeline as leverage. Corrective: run the audit yourself, from the lockfile, on a schedule — it is a build-time job, not a project — and keep the three-line summary of every exception somewhere a lawyer can read it in five minutes.

Module 04 Versions as promises

You accept code from people you will never meet, on a schedule you do not control, into software you are accountable for. The only thing standing between that arrangement and chaos is a three-number convention: a maintainer changes a digit, and the digit they change tells you what they believe they did to you. That is the entire mechanism. It is remarkably load-bearing for something with no enforcement whatsoever.

This module takes semantic versioning seriously as a protocol — what each position pledges, what a version range delegates, and the four distinct ways the pledge fails in practice. Getting precise here is what lets the next module explain why a record of what you actually installed is not optional.

Three numbers, three pledges

MAJOR.MINOR.PATCH encodes a claim about compatibility. A patch bump (2.3.1 → 2.3.2) pledges: same interface, same behavior, a defect fixed. A minor bump (2.3.1 → 2.4.0) pledges: things were added, nothing was removed or changed — code written against 2.3.1 still works. A major bump (2.3.1 → 3.0.0) is not a pledge but an announcement: the contract changed, read the migration guide, budget time. Version 0.x is outside the system entirely — the specification treats anything before 1.0 as unstable, so a 0.9 → 0.10 bump can break everything and remain perfectly compliant.

What makes this valuable is exactly what makes it fragile: it is a claim made unilaterally by the author about their own work, with nobody checking. Nothing verifies it. No registry rejects a breaking change published as a patch. The number is a maintainer's assertion of intent, and its accuracy depends entirely on their care, their test coverage, and their knowledge of how you use their code — which is zero.

Cross-reference — Guide Nº 18

Guide Nº 18 (how software ships) covers the mechanics from the publisher's side: cutting a release, tagging, changelog discipline, and the fact that what reaches the registry is an artifact built by a pipeline rather than the source tree you can read. This module is the same protocol viewed from the consumer's chair, where you receive the number and must decide what it entitles you to assume.

The semantic version structure with each position's pledge annotated, beside a timeline in which a patch release accepted through a caret range silently breaks a downstream build overnightThe promise2 . 3 . 1MAJOR — "I broke the contract. Re-read it."MINOR — "I added. I removed nothing."PATCH — "Same behavior. A bug is gone."No one verifies any of this. The number isan assertion of intent by one stranger.The break, one Tuesday nightMon 09:00 — build green onchart-kit 2.3.1manifest says"chart-kit": "^2.3.0"Mon 23:40 — maintainer publishes2.3.2 "fix: stricter date parsing"Tue 02:15 — CI re-resolves, takes 2.3.2no commit merged, no human involvedTue 08:05 — cellar-history 500steam debugs its own code for 3 hoursThe release was semver-compliant: it fixed a real bug. The pledge is about the maintainer's intent, not about your code.
Figure 4.1 — The semver promise, and the break. Each position pledges something specific and nothing verifies the pledge. On the right, the failure in one frame: a caret range standing open, a well-intentioned patch published at midnight, an unattended re-resolve at 02:15, and a production error at 08:05 that the team spends three hours hunting in code they never changed.

Ranges: trusting the future

^1.4.2 is not a version. It is a standing order: accept any release this maintainer publishes with the same major version, without asking me. ~1.4.2 is a narrower one — patches only. * or an unbounded range is a blank cheque. Writing a range means delegating a future decision to someone whose future behavior you cannot observe, and the width of the range is exactly the width of the delegation.

That is not automatically wrong. A range is how security patches reach you without a human in the loop, and pinning everything at exact versions forever produces its own well-documented disease (m8). But a range should be chosen, and it currently is not: caret is the default your package manager writes when you install, so the delegation width of most dependencies in most projects was picked by a tool, silently, at the moment of installation.

The useful move is to make range width a function of what you know about the maintainer — which is exactly what m2 taught you to assess. A foundation-hosted project with a documented release process and long deprecation windows has earned a wider band than a solo-maintained package whose changelog is a git log dump. Same syntax, different trust, deliberately chosen.

Version line showing exact pin, tilde band, caret band, and unbounded range drawn as intervals, each annotated with what it delegates1.4.21.4.91.9.02.0.03.x1.4.2exact pin — nothing enters uninvited; you own every upgrade~1.4.2patches only — you accept bug fixes sight unseen^1.4.2patches + featuresthe default your package manager writes for you, silently, at install timeunbounded — you accept the next rewrite, on the maintainer's schedule, at 02:00
Figure 4.2 — Ranges as trust bands. Each range is an interval over future releases and a standing order to accept whatever lands inside it. The width is the width of the delegation — and since caret is what the tooling writes by default, most projects delegated the majority of their dependency decisions without anyone choosing to.

Every way the promise breaks

Four failure modes, and they are genuinely distinct.

The accidental breaking patch. The maintainer fixes a real bug, believes correctly that they changed nothing in the interface, and breaks you anyway. This is the wine-cellar case. chart-kit 2.3.2 was titled "fix: stricter date parsing" and it did exactly that — it stopped accepting a malformed date string that 2.3.1 had silently coerced. The cellar-history page had been sending that malformed string for eleven months. The maintainer fixed a bug; the bug was load-bearing for us.

Hyrum's law. The general form of the above: with a sufficient number of users, every observable behavior of your system will be depended upon by somebody, regardless of what you promised in the interface. Undocumented behavior, error message text, iteration order, timing, the exact shape of a thrown exception — all of it is somebody's contract. Which means, uncomfortably, that a maintainer cannot ship a change that breaks nobody. Semver describes the interface; users depend on the implementation.

0.x semantics. Adopting a 0.9.x library "because it's nearly 1.0" applies post-1.0 pledges to a project that has explicitly declined to make them. The version number is a statement that the author is not yet ready to promise stability, and 0.9 → 0.10 may break everything while remaining fully compliant.

Tagging by feel. Some maintainers do not practice semver rigorously. They bump patch for anything small-feeling, minor for anything they are proud of, and major almost never because major bumps look disruptive. You cannot detect this from the version number — only from the changelog and from history, which is exactly what this module's worksheet asks you to go read.

The load-bearing idea

Semver is a promise about the maintainer's intent regarding their interface. Your build depends on the behavior of the implementation. Those are different objects, and the gap between them is permanent — which is why the record of what you actually installed (m5) has to exist independently of what you asked for.

Consuming versions defensively

The consumer's posture follows from the four failure modes. Treat patch as low-risk but non-zero — take them promptly, but let CI actually run before they reach production. Read the changelog at minor: additions can change resolution for your other dependencies and occasionally arrive with quiet behavior changes. Budget real time at major; the maintainer told you to, and the cost of deferring compounds (m8's pin-and-rot). And for 0.x dependencies, treat every bump as potentially major, because the author said so.

The two failure postures are symmetrical and both common. Floating wide means you find out about upstream changes when production tells you. Pinning everything and never revisiting means you accumulate a migration cliff and take your security patches under duress. Neither is a strategy; m8 builds the actual one, which is pinned versions plus a deliberate, automated cadence of reviewed updates.

Notice what none of this gives you: certainty about what you are running right now. A range says what you would accept. A changelog says what the maintainer thinks they did. Neither is a record. That record is the subject of the next module, and the wine-cellar's 02:15 re-resolve is exactly the event it would have prevented.

The anti-pattern, in both directions

Float-everything: symptom — a red build with no merged commits, and nobody's first hypothesis is the dependency tree. Pin-everything-forever: symptom — a critical CVE arrives and the fix requires crossing four major versions of a framework nobody on the current team has migrated before. Corrective for both: pin exactly, update on a cadence, and let automation propose the bumps so review capacity goes to the ones that deserve it.

Module 05 What you actually ran

Module 4 ended on a gap: a range says what you would accept and a changelog says what a maintainer thinks they did, but neither tells you what is running in production right now. That question — what did we actually install? — turns out to be the hinge of the whole discipline. It is the question a license audit answers from, the question a vulnerability scanner queries, the question an incident responder asks first, and the question that separates a forty-minute assessment from a panicked afternoon.

The answer is the lockfile, and the reason it deserves a module of its own is that most teams have one, commit it, and still cannot answer the question — because their CI quietly resolves around it.

Resolution: constraints in, tree out

An installer is a constraint solver. You hand it ranges — ^2.3.0 here, >=1.4,<2 there — and it must find one concrete version of every package in the transitive closure that satisfies every constraint simultaneously, including constraints imposed by packages on each other. This is genuinely hard; some ecosystems make it NP-hard in the general case, which is why installs can be slow and why resolution errors are so often opaque.

The critical property is the second input: registry state at the moment of the solve. Your manifest is one input; what has been published, deprecated, or yanked by the time you run the command is the other. Nothing in the manifest is a function of time, and the output is entirely a function of time. Run the same install today and next Thursday and you may get two different programs from identical source.

Concretely

The wine-cellar front end declares seven dependencies. Between Tuesday and Thursday, four transitive packages published releases inside their existing ranges — a logging helper, a polyfill, a type-definition package, and chart-kit's date utility. Neither manifest changed by a character. The resolved trees differ in four places, one of which changes how a date string renders. Nobody made a decision; time simply passed.

Manifest is intent; lockfile is the record

The two files are frequently confused and do entirely different jobs. The manifest is human-edited and states intent: these are the packages I want, and these are the future versions I am willing to accept. The lockfile is tool-generated and states the record: this is the exact tree the solver produced — every package, direct and transitive, at an exact version, with an integrity hash for each artifact.

Those hashes are the underrated half. A lockfile without them pins names and numbers; with them, it pins bytes. If a registry serves you different content under the same version — because a package was republished, a mirror was compromised, or a proxy is misbehaving — the hash check fails and the install stops. That is your only structural defense against "the version number is right and the code is not," which is precisely the m6 attack.

Once the lockfile exists and is honored, you can install from it rather than solving again: the tool reads the recorded tree and fetches exactly those artifacts, verifying hashes, with no solve and no registry-state dependency. This is the difference between the npm ci and npm install families of command, and between pip install -r requirements.txt with fully pinned hashes and without. The distinction is not stylistic. One reproduces a build; the other performs a fresh act of trust every time it runs.

Layered diagram showing manifest as intent above lockfile as record above the installed tree, with regeneration as the only sanctioned mutationManifest — intent"chart-kit": "^2.3.0"human-edited · ranges · what I am willing to acceptsolve, once, deliberatelyLockfile — the recordchart-kit 2.3.1 · sha512-9f2c…tool-generated · 1,161 exact versions + integrity hasheswhat I actually got — the source of truthinstall from it · verify hashes · no solveInstalled tree — identical everywherelaptop = CI = staging = productionregenerate:a reviewedact, nevera side effectEvery arrow that skips the middle layer is a fresh act of trust in whatever the registry serves that minute.
Figure 5.1 — Intent vs. record. The manifest declares what you will accept; a single deliberate solve produces the lockfile, which records exactly what you got, hash by hash; installs then read the record rather than re-solving, so every environment receives the identical tree. Regenerating the lockfile is the one sanctioned mutation, and it is a reviewed act rather than a side effect of running a command.

The drift bug, reproduced and cured

The wine-cellar incident is worth walking in full, because its shape recurs everywhere. CI was green on Tuesday afternoon. The Thursday production deploy — same commit, tested, approved — rendered every vintage-year axis label as Invalid Date on the cellar-history page. Nothing in the diff touched date handling. Nothing in the diff touched anything, in fact: it was a copy change to the footer.

The team spent two hours on application code before someone compared the installed tree in the production image against the tree in the CI container. They differed in four packages. One was chart-kit's date utility, which had published a release inside its range on Wednesday. The Thursday deploy built a fresh image, and the fresh image re-solved, and the re-solve took the new version. The tested artifact and the deployed artifact were never the same program — they were two different resolutions of the same intent, forty hours apart.

Side-by-side contrast: without a lockfile the same manifest resolves to two different trees on Tuesday and Thursday; with a lockfile both installs produce the identical treeWithout a lockfile — intent onlymanifest: "^2.3.0"Tue · CIThu · prodchart-kit 2.3.1dates 1.8.2polyfill 3.1.0logger 0.9.4chart-kit 2.3.1dates 1.9.0polyfill 3.2.1logger 0.10.0two programs · one commit · 40 hours apartyou tested the left one and deployed the right oneWith a lockfile — the recordlockfile: 2.3.1 · sha512-9f2c…Tue · CIThu · prodchart-kit 2.3.1dates 1.8.2polyfill 3.1.0logger 0.9.4chart-kit 2.3.1dates 1.8.2polyfill 3.1.0logger 0.9.4one program · verified byte-for-byte by hashyou deployed the artifact you testedThe manifest is identical on both sides. The only difference is whether the install re-solves or reads the record —which is a choice made by one word in your CI config and your Dockerfile, not by the presence of a file in git.
Figure 5.2 — Same manifest, two trees; one lockfile, one tree. On the left, an unchanged manifest resolved twice forty hours apart yields three different transitive versions, so CI validated a program production never ran. On the right, both installs read the recorded tree and verify hashes, producing byte-identical results. "Works on my machine," reproduced and then cured — and the cure is which install command runs, not whether a lockfile exists in the repository.

The cure has two parts and only one of them is the file. Commit the lockfile, yes. But the decisive change is that every install in CI and in the production image build reads from the lockfile rather than re-solving, so the artifact you tested is byte-identical to the artifact you deploy. The wine-cellar team had been committing a lockfile for two years. Their Dockerfile ran the fresh-resolve install command the whole time.

Cross-reference — Guide Nº 16

Guide Nº 16 (where code runs) makes the general case: an environment is everything your code depends on that is not your code, and reproducibility means pinning all of it — base image, runtime version, system libraries, and dependency tree. The lockfile is that principle applied to one layer. "Works on my machine" is the same failure every time — an input you did not know you had, differing between two environments — and version drift is simply its most industrialized form, because the difference arrives on a stranger's publish schedule rather than from a colleague's laptop configuration.

The load-bearing idea

Testing an artifact you did not deploy is not testing. If your deploy pipeline re-resolves dependencies, your CI run validated a program that has never existed in production and never will — and the gap between them widens with every hour between the two solves.

Lockfile discipline

Four rules, and they are boring on purpose. Commit it. It is the record of what you ship, and a record that lives only on someone's laptop is not a record. Install from it everywhere non-interactive — CI, image builds, production — using the command that fails rather than repairs when the lockfile and manifest disagree. Regenerate deliberately: changing the lockfile is changing what you ship, so it happens in its own pull request with a stated reason. Review the diff proportionally — not line by line for a 40,000-line regeneration, but by checking that the shape matches the intent, because a one-package bump that rewrites the whole tree is telling you something.

The two anti-patterns are asymmetric. Having no lockfile at all is bad and, crucially, honest — nobody believes the build is reproducible, so nobody relies on it. Having a committed lockfile that CI ignores is worse, because it is a false attestation: the file is in the repository, the audit sees it, everyone believes the build is reproducible, and it is not. This is a control that appears to exist. In your GRC vocabulary, that is the difference between an unimplemented control and one that fails silently while passing inspection — and the second is the one that causes the incident nobody saw coming.

The anti-pattern

Rubber-stamping lockfile regeneration. Symptom: a PR titled "bump one dependency" carries a 40,000-line lockfile diff and is approved in ninety seconds because "it's generated." Corrective: read the summary, not the lines — how many packages changed, which majors moved, were any new packages introduced. A one-package bump that reshapes a third of the tree usually means a wide range somewhere relaxed and let a great deal in with it. That is a decision, and somebody should make it on purpose.

Module 06 The dependency as attack surface

Everything so far has treated the tree as a source of accidents: obligations you did not read, promises that broke, versions that drifted. This module treats it as a target, because it is. An adversary who compromises your application gets your application. An adversary who compromises a package your application depends on gets your application and every other one that pulls it — and does so through the pipeline you built to deliver software to yourself.

The economics are the whole story, and they are not subtle. Attacking one organization is a project. Attacking one package that eleven thousand organizations install unattended, with build systems that run its code on machines holding deployment credentials, is the same project with a vastly better return. So that is where the effort went.

Why attackers moved upstream

Consider what happens on a developer laptop or a CI runner when an install runs. Packages are fetched from a registry over the network, unpacked into your workspace, and in several major ecosystems permitted to execute install scripts — arbitrary code, running as your user, before you have executed a single line of your own program. That process typically has your source tree, your environment variables, your cloud credentials, your registry tokens, and network egress. Then the code ships to production and runs again, in-process, with your database.

So a supply-chain attack — compromising a target by compromising something it depends on — buys the attacker two distinct footholds from one break-in, multiplied across every downstream consumer. Against that, your organizational security posture is largely irrelevant: your SSO, your code review, your penetration test, and your secure SDLC all sit downstream of the moment your build fetched a stranger's tarball and ran its setup script.

Cross-reference — Guide Nº 23

Guide Nº 23 (application security) frames everything around trust boundaries: where does data or code cross from a place you control into one you do not, and what is checked at the crossing? The install is the boundary most teams never drew. Nothing is validated, nothing is sandboxed, and the crossing happens hundreds of times per build. Drawing that boundary explicitly — and asking what is checked there — is most of this module's value.

The load-bearing idea

Your build pipeline is a remote code execution service that you operate, on behalf of strangers, on machines holding your production credentials. Every control in this module is an attempt to put something — a pin, a hash, a scan, a human — between a stranger's publish button and that execution.

The attack taxonomy

Four families, distinguished by whether the package was ever legitimate and by where it enters.

Known-vulnerable components. Nobody attacks the package; everyone attacks its users. The package is honest, the maintainer is honest, a vulnerability is found and published, and the window between disclosure and your patch is the attack. Log4Shell in December 2021 is the reference case: a trivially exploitable flaw in a logging library so ubiquitous that most affected organizations could not answer whether they were affected — which is the m5 and m8 question, not a security question.

Typosquatting. Publish a malicious package under a name close to a popular one and wait for fuzzy fingers and fuzzy memories. The victim's mistake is installing by approximate recall rather than by copying an exact name from documentation. Cheap for the attacker, and it works at a low rate on an enormous population.

Dependency confusion. The most elegant of the four. Your company has an internal package — say wc-billing-utils — hosted on a private registry. An attacker publishes a package with that exact name on the public registry at version 99.0.0. If your resolver is configured to consult both and prefers the highest version, it fetches the attacker's. The victim's mistake is a resolver-precedence assumption nobody ever wrote down, and the attacker needs only your internal package names, which leak constantly through job postings, error messages, and published bundles.

Compromised legitimate updates. A real package with real users ships a real release containing a payload. This is the worst family because every signal you have says trusted: the name is right, the maintainer is the same person, the download counts are high, the history is years long, and the repository looks clean. event-stream in 2018 is the accessible version — an exhausted maintainer handed publish rights to a helpful stranger who shipped a wallet-stealing payload weeks later. xz in 2024 is the sophisticated version.

Grid comparing four supply-chain attack families by entry point, victim mistake, and primary controlFamilyEntry pointMistake exploitedPrimary controlKnown-vulnerablepackage always honestLog4Shell, 2021already in your treewhen disclosure landsslow patching; notknowing what you shipscanning + SBOM+ update cadenceTyposquattingnever legitimateinstall command,typed by a humaninstalling by fuzzyrecall, not exact namecopy names from docs;review new-package PRsDependency confusionnever legitimateresolver consults thepublic registry toounwritten precedenceassumption; leaked namesscoped names; pin theregistry per namespaceCompromised updatelegitimate, then notevent-stream · xza normal release,inside your rangetrusting earned trust;unattended updatespins + hashes + delay;restrict install scripts
Figure 6.1 — The four attack families. The families split on whether the package was ever legitimate: known-vulnerable components are honest code with a published flaw, typosquats and dependency confusion are hostile from birth, and compromised updates are the hard case because a real package with a real history turns. Each family enters at a different hop, so each has a different primary control — and no single control covers more than two of the rows.

The xz story

xz-utils is a compression library. It is not glamorous, it is everywhere in Linux distributions, and in early 2024 it was maintained largely by one person who had been doing it for years, unpaid, while dealing with long-term health difficulties he had mentioned publicly on the mailing list.

Starting in 2021, an account under the name "Jia Tan" began contributing. The patches were competent and useful. Over roughly two years the account built a reputation as a reliable contributor — while, in parallel, other accounts appeared on the mailing list to complain about slow releases and to press for a co-maintainer, in messages that read in hindsight as manufactured pressure applied to an exhausted man. By 2023 the campaign had what it needed: commit and release authority.

The payload shipped in versions 5.6.0 and 5.6.1 in early 2024, and its construction is the part worth studying. The malicious code was not in the git repository. It was concealed in the release tarballs — the artifacts distributions actually build from — hidden inside binary test fixture files and assembled during the build by a modification to the build scripts. It targeted the SSH daemon indirectly, through a linkage relationship, to install a backdoor for an attacker holding a specific key. Anyone reading the source on GitHub would have seen nothing.

It was caught because Andres Freund, a Postgres developer running Debian's unstable branch, noticed SSH logins taking about half a second longer than expected and, being the sort of person who does not let that go, profiled it. Distribution releases were weeks away. The near-miss was extraordinarily close.

Timeline of the xz-utils compromise from 2021 contributor onboarding through 2024 discovery2021202220232024contributorappearsuseful patches;trust accruespressure campaign ona burned-out maintainercommit + releaseauthority granted5.6.0 / 5.6.1: payload intarballs, not in gita 500ms login delay,profiled by one engineerRoughly three years of patience — the axis that makes this hard to defend
Figure 6.2 — The xz timeline. A contributor account spent two years earning legitimate trust through useful work while coordinated social pressure pushed an exhausted solo maintainer toward accepting help; authority was granted in 2023 and the backdoor shipped in early 2024, concealed in release tarballs rather than in the readable repository. It was found by accident, weeks before wide distribution uptake, because one engineer investigated half a second of unexplained latency.
What xz should change in your reasoning

Three things. "Trusted for years" is a statement about the past, not a property of the next release. "The source looks clean" can be true of a compromised artifact, because the artifact is built, not read (Guide Nº 18). And the maintainer sustainability problem from m2 is not merely a continuity risk — it is an attack surface, deliberately targeted, because an exhausted volunteer is a person who can be pressured into accepting help.

Where defenses actually intercept

Controls are worth exactly the hops they cover, and the honest version of each is more useful than the marketing one.

Pinning and lockfiles (m4, m5) stop resolution-time substitution: a new malicious release cannot enter unattended, because nothing enters unattended. They do not help if the compromised version is the one you pinned. Integrity hashes stop content substitution under a version you already accepted — registry compromise, mirror tampering, republication. They say nothing about whether the original content was malicious. Scanning (m8) catches versions that are known bad, which is a strictly historical property: it would have flagged xz 5.6.1 the day after disclosure and would have said "clean" every day before. Restricting install scripts and running builds with least privilege shrink the blast radius of execution rather than preventing it — a package that cannot reach your cloud credentials is a much smaller problem than one that can. Delaying adoption of brand-new releases by a few days is unglamorous and genuinely effective against this class, since malicious releases tend to be caught in days. And human review of dependency changes is the last net, with a sharp limit: reviewing the diff of a package you are adding is feasible, reviewing 260 changed packages is not, and reviewing an artifact whose payload lives in a binary test fixture is not either.

The uncomfortable summary is that no control on this list would have caught xz before Freund did. That is not an argument against controls; it is an argument for layering them and for being precise about what each one buys, because a security program built on a control you have misunderstood is worse than one built on fewer controls you have understood correctly.

Swimlane sequence tracing a compromised update from attacker through registry, transitive tree, victim build, and victim runtime, with interception points marked at each hopattackerregistryyour treeyour buildyour runtimeearns publishrights (2 years)publishes 5.6.1payload in tarballresolved at depth 3inside an open rangeinstall script runswith CI credentialspayload in prodwith your datapin + lockfileblocks this hop entirely— unless you pin the bad oneintegrity hashblocks content swaps— not a malicious originalscannercatches known-bad only— silent until disclosurescript restrictions+ least privilege— shrinks, never preventshuman reviewthe last net— blind to binary fixtures
Figure 6.3 — Anatomy of a supply-chain attack. A compromised update travels attacker → registry → resolved tree → build → runtime, and each control intercepts exactly one hop: pins and lockfiles stop unattended entry, hashes stop content substitution, scanners flag only what is already disclosed, script restrictions and least privilege shrink what execution reaches, and human review covers what a human can actually read. Reading the qualifier on each is the point — layered controls with understood limits beat a single control you have overestimated.

Module 07 Deciding to adopt

Six modules of consequences converge here into one decision made at one moment: somebody opens a pull request adding a line to a manifest. Everything this course has covered — the transitive tail, the maintainer, the license, the version discipline, the attack surface, the reproducibility — is priced at that moment or discovered later at a markup.

This module is not an argument against dependencies. The reflexive build-it-yourself position is its own failure mode, and an expensive one: teams that write their own date handling, their own crypto, and their own timezone logic produce bugs the ecosystem solved a decade ago. The goal is a defensible decision in either direction, which means gates you can state out loud and evidence you actually gathered.

The true cost of a dependency

The code is free. The relationship is not, and it is priced across six dimensions you now have names for. You are adopting the package's transitive tail — every package it pulls, which is the real unit of what you added (m1). Its maintainer's continuity — their capacity, governance, and probability of still being here in three years (m2). Its license terms, and its dependencies' license terms, which you will not have read (m3). Its release discipline — whether the version numbers mean what they claim (m4). Its share of your attack surface — install-time and runtime execution, in your build and in production (m6). And an exit cost that grows with every file that imports it.

That last one is the one nobody prices at adoption time, and it is the one that determines whether a bad outcome is annoying or expensive. Adoption is cheap and reversible on day one, cheap and irreversible-in-practice on day four hundred. The cost curve is entirely a function of decisions you make in the first week — chiefly, whether the dependency is imported directly across the codebase or reached through an interface of your own.

Note

None of this argues for fewer dependencies as such. It argues that the number in your head when you evaluate a library — "it's 40KB and does what I need" — is measuring one of seven costs. A good decision can absolutely be "adopt, with eyes open, and here is what we accepted."

Reading health beyond the star count

Stars and download counts are lagging indicators. They record adoption decisions other people made in the past, often years ago, sometimes on the strength of a conference talk. A package with 41,000 stars and no release in twenty-six months is not healthy; it is famous. The two are routinely confused because fame is the metric that appears at the top of the page.

The leading indicators take about ten minutes to gather and actually describe the present. Release recency and cadence — when did anything ship, and is the rhythm steady or collapsing (m2's chart-kit profile)? Issue-response latency — not how many issues are open, which mostly tracks popularity, but how long until a maintainer responds at all; a queue of 300 issues with same-week responses is a healthy project, and 40 issues with none is not. Bus factor from publish history — how many distinct people have cut a release in two years. The package's own dependency weight — what it drags in. Changelog quality, which is the single best proxy for whether the maintainer thinks about downstream consumers at all: a changelog that flags behavior changes is a maintainer who knows you exist. And documented security handling — is there a stated way to report a vulnerability, and evidence anyone answered one?

The anti-pattern

Trusting stars and downloads as health. Symptom: the adoption discussion cites popularity metrics and nothing else, and nobody has opened the changelog. Corrective: ban popularity from the evaluation entirely for one review and see whether the case survives on leading indicators alone. It usually does — and when it doesn't, you have learned something for ten minutes of effort.

Blast radius before you sign

The question to ask before adopting is not "is this good?" but "what breaks, and how hard is it to leave?" Two axes answer it. Coupling depth: how much of your code touches this directly — one module, or two hundred files. Replaceability: does a comparable alternative exist, and does the package's interface resemble anything else, or is its model idiosyncratic enough that switching means rewriting your own logic around it?

The quadrants that fall out are useful because they prescribe different actions. Trivial to drop — shallow coupling, replaceable — adopt freely; a bad outcome costs an afternoon. Annoying — deep coupling, replaceable — adopt, but the wrapper pays for itself. Load-bearing — shallow coupling, hard to replace — adopt with a named migration path, because you accepted a real dependency deliberately. Regret — deep coupling, hard to replace — this is where you must have done the m2 governance work, because a solo-maintained package in this quadrant is a bet on one person's continued interest, with your product as the stake.

The wrapper module is the cheapest risk control in this course: import the dependency in exactly one file of your own, expose the narrow interface your application actually needs, and have everything else call yours. It costs an hour at adoption. It moves a dependency an entire quadrant to the left, converting "rewrite 200 call sites" into "reimplement one adapter." It also, incidentally, makes the dependency testable and its behavior mockable, which is why the practice survives even when nobody is thinking about supply chains.

Quadrant chart plotting coupling depth against replaceability, with wine-cellar dependencies positioned in the trivial, annoying, load-bearing, and regret quadrantsHarder to replace →Deeper coupling →Trivial to dropadopt freelyLoad-bearingadopt with a named exitAnnoyingwrap it; the hour pays for itselfRegretonly with governance you trustpdf-generatorhttp-client (wrapped)flaskreactchart-kit23 files · solo · driftingcandidate: fancy-dates,placed before adoptionA wrapper moves a package a full quadrant left for about an hour of work — which is why it is priced at adoption, not later.
Figure 7.1 — Blast-radius quadrants. Coupling depth against replaceability sorts a portfolio into four prescriptions: adopt freely, wrap it, adopt with a named exit, or require governance you actually trust. Wine-cellar's chart-kit sits in the regret quadrant — imported directly across 23 components, idiosyncratic enough that no drop-in alternative exists, and solo-maintained with a collapsing cadence — which is the combination that turns an ordinary dependency into a project.

Build vs. borrow, honestly

Run the wine-cellar case to a terminal. The need: parsing vintage windows from user input — strings like "1998–2004", "NV", "early 90s" — into a year range for the cellar-history filter. The candidate: fancy-dates, a well-regarded natural-language date parser that handles this and a great deal more.

Gate 1, is the need real and nontrivial? Real, yes. Nontrivial — partly. The general problem of natural-language date parsing is genuinely hard. Our problem is four input shapes drawn from a domain where an invalid parse is a filter that returns nothing, not a correctness disaster. Pass, with a note.

Gate 2, health. First release fourteen months ago, cadence steady, single publisher, changelog present but terse and not flagging behavior changes, no documented security-report process. Marginal pass — this is a promising young project, not an established one.

Gate 3, license. MIT, and the tree is permissive throughout. Clean pass.

Gate 4, transitive weight. 87 packages, including a full locale database and a timezone dataset, for a feature that only ever handles four-digit years. This is where it fails, and it fails on the metric that Figure 1.1 taught us to look at rather than the one on the package page.

The alternative. Four regular expressions and a small lookup table for the decade shorthands — about twenty lines, with roughly thirty lines of tests enumerating the shapes we accept and the shapes we reject. Half a day to write. It handles exactly our four cases and fails visibly on anything else, which for a filter input is the correct behavior.

Terminal: write it yourself. And note the reasoning that produced it, because it is not "dependencies are bad." It is that the ratio of borrowed generality to needed generality was roughly 87 packages to 20 lines, and the domain tolerates a narrow, visible failure. Change one fact — make it timezone-aware scheduling across locales, where the correct behavior is genuinely subtle and our own bugs would be silent and wrong — and the same flowchart terminates at adopt, with a wrapper.

Adoption decision flowchart with five gates ending in adopt, vendor, or write-it-yourself terminals, with the fancy-dates candidate path tracedcandidate: fancy-dates1 · need real and nontrivial?pass — with a note2 · health signals pass?marginal — young, solo, terse changelog3 · license compatible?clean — MIT throughout4 · transitive weight acceptable?fail — 87 packages for 4 input shapesno5 · blast radius bounded?not reachedadoptall gates pass — wrap it if coupling will be deepvendorneed it, cannot trust the source — you own it nowwrite it yourself20 lines + 30 lines of tests · half a dayChange one fact — make it timezone-awarescheduling — and the same chart terminatesat adopt. The gates, not the instinct, decide.
Figure 7.2 — The adoption decision. Five gates — real need, health, license, transitive weight, blast radius — routing a candidate to adopt, vendor, or write-it-yourself. The fancy-dates evaluation passes need, health, and license, then fails on 87 transitive packages bought for four input shapes, and terminates at twenty in-house lines. The same chart adopts the same library if the problem is timezone-aware scheduling, which is the point: the gates decide, not a preference.

Module 08 Living with dependencies

Adoption is a moment; stewardship is the rest of the relationship, and it is where dependency management is actually won or lost. A team that adopts thoughtfully and then never touches the tree again arrives, eighteen months later, at the same place as a team that never thought about it at all — except with better documentation of how they got there.

The engineering problem in this module is a genuine dilemma with two bad extremes and a middle that only works if it is automated. The organizational problem is that stewardship is invisible when it works, which is why it loses budget arguments to features until the quarter it doesn't.

The update dilemma

Float wide and you have delegated future decisions to strangers, which m4's 02:15 incident illustrated: unreviewed code enters your build on a maintainer's schedule and you learn about it from production. Pin everything and never revisit, and you get the opposite disease — slower, quieter, and worse. Known vulnerabilities accumulate in a tree you have frozen. The gap between your version and current widens. Each deferred major makes the next one harder, because migrations compose. And then a critical CVE lands, the fix exists only in a version four majors ahead, and you attempt the migration you have been deferring for two years during an incident, under time pressure, with a security clock running.

Both extremes are attempts to make the update decision go away — one by delegating it, one by refusing it. The resolution is to keep the decision and make it cheap: pin exactly, so nothing enters uninvited, and run a deliberate cadence of reviewed updates so the pins never get old. That combination requires automation, because the manual version competes with feature work every single week and loses.

The anti-pattern

Pin-and-rot. Symptom: a dependency audit shows median age of your pinned versions measured in years, and at least one framework is more than two majors behind. Corrective: measure the gap deliberately — for each direct dependency, how many majors and how many months behind are you? Publishing that number quarterly is usually enough to get the work scheduled, because it converts a vague unease into a metric that visibly gets worse while nobody acts.

Automating the toil

The mechanism is unglamorous and works: an update bot — Dependabot, Renovate, or equivalent — watches the registry, opens a pull request per available bump, and CI exercises each one. A human merges. That is it. What changes is not the amount of work but its shape: updates become small, frequent, individually reviewable, and boring, instead of rare, enormous, and terrifying.

Routing is where the design happens. Patch bumps on dependencies you trust can auto-merge on green CI — the risk is low, the volume is high, and human attention spent here is attention not spent where it matters. Minor bumps get a human glance at the changelog; a fifteen-second look catches the "we also changed X" entries. Major bumps never auto-merge; the bot's PR becomes a ticket that enters normal planning, which is exactly the point — the migration gets scheduled rather than deferred. Security bumps jump the queue regardless of type and page someone if severity warrants.

Two failure modes to design against. Bot fatigue: forty PRs a week trains everyone to ignore the bot, so group updates into batches and set a cadence — a weekly batch beats a daily trickle. And auto-merge without teeth: auto-merging patches is only safe if CI would actually catch a regression, so the honest question is whether your test suite exercises the paths your dependencies touch. If it does not, the correct configuration is a human on every merge, and the correct project is better tests.

Swimlane sequence showing patch, minor, major, and security bumps flowing from registry through an update bot and CI to a human reviewer and main branch, each on a different pathregistryupdate botCIhumanmainpatchbatched PR, weeklygreen → auto-mergeminorgreen15s changelog readmajornever auto-merges —becomes a planned migration ticketsecurityjumps the queue, any bump typetriage now (m9)Auto-merge is only as safe as the test suite behind it. If CI would not catch a regression in the paths yourdependencies touch, the correct setting is a human on every merge — and the correct project is better tests.
Figure 8.1 — The update pipeline. The bot watches, CI exercises, a human decides — with the routing carrying the design: patches batched and auto-merged on green, minors given a fifteen-second changelog read, majors converted into planned migration tickets that never merge unattended, and security bumps jumping every queue. The result is that updates become small and boring rather than rare and terrifying.

Scanning for what's already known

A vulnerability scanner joins two lists: your lockfile — the record of what you actually have (m5) — against public advisory feeds like CVE and GHSA. That is the whole mechanism, and understanding it tells you exactly what scanning buys. It finds what the world already knows is bad in what you provably have. It says nothing about undisclosed flaws and nothing about deliberately malicious code (m6), and a clean report means "no match today," not "safe."

The craft is triage, because raw output is unusable. A first scan of a mature tree routinely returns hundreds of findings, and a team that treats all of them as equal quickly treats all of them as noise. Three questions rank them. Severity in your context, not the published score: a denial-of-service rating on a parser you feed only your own configuration files is not a nine. Reachability: is the vulnerable function called from any path your application actually executes? Does it ship? A finding in a build-time tool is a different risk from a finding in your production runtime — different, note, not absent, because m6 established that the build stage holds deploy credentials and is a target in its own right.

That ranking produces three actions: patch now, accept with a documented reason and a review date, or defer to the normal cadence. Writing the accepted ones down is what separates triage from dismissal, and it is what makes the same finding cheap to handle the fourth time it appears.

From your GRC work

This is continuous monitoring against an asset inventory, with the lockfile as the inventory and the advisory feed as the threat intel. Every failure mode you already know transfers directly: an inventory that does not match reality makes the monitoring worthless, unranked alerts train responders to ignore the queue, and exceptions without expiry dates become permanent by default. The novelty here is only that the assets are packages and the inventory regenerates on every build.

The anti-pattern

Scanning without triage. Symptom: 340 open findings, none assigned, the dashboard bookmarked by nobody, and a team that has learned the alerts do not mean anything. Corrective: rank once by severity × reachability × ships, act on the top tier, and formally accept the rest with dates. A queue of 12 real items that people read beats 340 that nobody does — and the exercise usually reveals the real number was never 340.

Knowing what you ship: the SBOM

A software bill of materials is a machine-readable inventory of every component in a built artifact: names, versions, licenses, hashes, and the relationships between them. Two standard formats dominate — SPDX and CycloneDX — and the practical detail is where it comes from: an SBOM is generated at build time from the resolved tree, which is to say from the lockfile. It is the lockfile's record, made portable and attached to the artifact rather than living in the repository.

That attachment is what makes it worth the trouble. A lockfile in git tells you what the commit intended; an SBOM attached to image sha256:9f2c… tells you what that artifact contains, which is the thing actually running. During Log4Shell, the organizations that spent days on the question were not incompetent — they simply had no queryable inventory of what was inside their deployed artifacts, so "are we affected?" became archaeology across dozens of services and images.

With an SBOM per artifact, that question is a query, and it answers three things at once: whether the component is present, in which version, and in which deployed artifacts. It also generates your license notices file for free (m3) and gives your scanner something stable to run against. And increasingly you will be asked for one regardless — enterprise procurement and several regulatory regimes now request SBOMs as a matter of course, which means the cheapest time to start producing them is before somebody's questionnaire requires it.

Flow from build through lockfile to SBOM, matched against an advisory feed and routed through triage to patch, accept, or deferbuildlockfile — the recordSBOMattached to the artifactmatchagainst advisory feedCVE / GHSA feedtriageseverity in context ×reachable × ships?patchacceptdeferwith a reasonand a dateAlso generated freefrom the same SBOM:· license notices (m3)· "are we affected?" (m9)· the customer questionnaire
Figure 8.2 — From lockfile to answer. The SBOM is the lockfile's record generated at build time and attached to the artifact, which is what makes it queryable about the thing actually deployed rather than about a commit. Matched against advisory feeds and passed through triage, it turns "are we affected?" from archaeology into a query — and produces the license notices and the customer questionnaire answer as by-products.

Module 09 When a dependency goes bad

Everything before this module was preparation for about forty minutes of work you will do a handful of times a year, usually starting with a headline or a Slack message and a spike of adrenaline. The quality of those forty minutes is almost entirely determined by decisions already made: whether the lockfile is honored, whether an SBOM exists, whether the affected dependency is wrapped, and whether anyone has written down who decides.

The module closes with the artifact this whole course has been building toward — a one-page dependency policy. Not because policies are inherently valuable; most are not. Because the difference between a calm response and a chaotic one is whether the decisions were made in advance by people who were not panicking.

The four bad days

They look similar from the outside and demand different first moves, so identifying which day you are having is the first triage decision.

A CVE drops. The package is honest, a flaw is disclosed, a patched version usually exists. First move: determine whether you are affected at all — the m8 triage, run against your SBOM. Most of the time, the answer that ends the incident is "present but not reachable and not shipped," and getting there in twenty minutes is the entire value of the preparation.

A package is yanked or abandoned. A version is withdrawn from the registry, or a maintainer announces the end. Nothing is running badly — your pinned builds keep working — but your ability to rebuild may be gone, which is a different and quieter emergency. First move: confirm you can still build from your lockfile, and check whether your artifact cache holds the packages you need. left-pad is the archetype, and the ecosystem's response — restricting unpublishing — is why this day is rarer now than it was.

A maintainer turns hostile. A deliberate protest release, sabotage, or a wiper disguised as an update. colors and faker in 2022 are the reference: working packages, published by their legitimate author, updated to break every consumer on purpose. First move: pin back to the last known-good version. Note the inversion — on this day newer is worse, and the reflex to upgrade is exactly wrong.

A build is compromised. A release contains a payload — event-stream, xz. This is the most serious because it is not merely a vulnerability, it is a probable intrusion: if you installed the malicious version, code ran with your credentials. First move: determine whether the malicious version ever entered any build or any developer machine, then treat it as a credential-compromise incident rather than a dependency problem.

The load-bearing idea

Two of the four days call for moving forward and two call for moving backward. If your team's only reflex is "upgrade to latest," you will do the right thing on the CVE day and the wrong thing on the hostile-maintainer day — and the wrong thing on the day you most need to be right.

Blast radius under pressure

Ask the questions in this order, because each one can end the incident and they get progressively more expensive to answer. One: is it in my tree at all? Query the SBOM or lockfile across every deployed artifact. Two: which versions, and where? Vulnerable range versus what you actually run, per service. Three: is the vulnerable path reachable? Do you call the affected function, or does it sit in a code path your usage never touches? Four: does it ship, or is it build-only? Different exposure, not zero exposure. Five: is exploitation active, and is a fix available? This determines urgency and whether you patch or mitigate.

Run the secondary example through it. A compromised update lands in the base image the wine-cellar backend builds on — an xz-shaped event: a long-trusted low-level component ships a backdoored release, and it reaches you through the container base rather than through your manifests, which is exactly why m6 insisted the boundary is where third-party code executes, not where you declared it.

Q1: does the SBOM for our deployed images contain the affected package? Yes — three of five images, at the affected version, present in the base layer. Q2: the digest-pinned images built before the compromise window are clean; two rebuilt on Wednesday are in range. Q3: reachable — it is linked into a component the SSH path uses, so we assume yes rather than trying to prove no under time pressure. Q4: it ships, and it also ran during the build. Q5: a fixed version exists and distributions have already reverted.

Total elapsed time: forty minutes, most of it spent reading rather than searching, because the SBOM answered Q1 and Q2 in one query and the digest pinning made the timeline unambiguous. The response: rebuild all images on the reverted version, redeploy, and — because malicious code executed on build runners — rotate the credentials that were present during those two builds. Without the SBOM and the digest pinning, Q1 and Q2 alone are an afternoon of exec-ing into containers, and every subsequent decision is made on shaky evidence.

Cross-reference — Guides Nº 16, 18, 23

Guide Nº 23 supplies the incident framing — assume compromise, size the blast radius, contain, then eradicate. Guide Nº 18 supplies the rollback mechanics that make "redeploy the previous good artifact" a five-minute operation rather than a project. Guide Nº 16 supplies the reason this arrived through a base image at all: your dependency tree does not stop at your manifests — the base image, the runtime, and the system libraries are dependencies too, and they deserve the same pinning discipline.

The remediation menu

Five options, each with a real cost and an honest failure mode.

Upgrade. Take the patched version. Cheapest when the fix is a patch release; expensive and risky when the only fix is across a major you have deferred (m8's cliff, arriving on schedule).

Pin back. Return to the last known-good version. The correct first move on the hostile-maintainer day and a useful stabilizer while you evaluate. Its failure mode is specific: pinning back to before a malicious release usually also reverts legitimate fixes, so if the last good version has its own known vulnerability, you have traded one exposure for another — and you must say so out loud.

Vendor and patch. Copy the source in, apply the fix yourself. Sometimes the only option when upstream is abandoned. You have now adopted the maintenance burden you avoided by depending in the first place (m7), permanently, and it will not be in anyone's mental model in six months. Do it deliberately, with an owner and a note explaining what was changed and why.

Replace. Switch to a different package. Correct when the incident revealed a governance problem rather than a bug — but it is a project, not a response, so it belongs on the plan after the immediate risk is contained.

Remove. Delete the dependency and the feature or code path that needed it. Underrated. If the finding is in a package supporting something used by four customers a month, removal can be the cheapest total-cost answer, and it is the only remediation that also shrinks the tree.

OptionBest whenReal costFailure mode
UpgradeA patch or minor fix existsHours, plus regression riskThe only fix is four majors ahead
Pin backHostile release; need to stabilizeMinutesRe-exposes bugs fixed since the good version
Vendor + patchUpstream abandoned, fix is smallDays now, forever afterOwnership is forgotten within two quarters
ReplaceGovernance failure, not a bugWeeksTreated as a response when it is a project
RemoveThe feature is low-valueHours to daysUnder-considered because nobody proposes it
Incident playbook flowchart routing four alert types through the ordered blast-radius queries to five remediation terminals, with the compromised base image path tracedWhich day is it?CVE disclosedyanked / abandonedhostile maintainerbuild compromised1 · in my tree? — query the SBOMno → close, record2 · which versions, which services?3 of 5 images, in range3 · reachable?assume yes under pressure4 · ships, or build-only?both — and it ran on CI5 · active exploitation? fix available?upgradepin backrebuild on revertedvendor + patchreplaceremoveTraced path: the base-image compromise — 40 minutes to a decision, then rotate every credential present during those builds.
Figure 9.1 — The incident playbook. Four alert types converge on one ordered query sequence, and each query can end the incident — which is why the SBOM query comes first and the reachability analysis comes third. The traced path is the base-image compromise: present in three of five images, assumed reachable, shipped and executed on CI, fix available — terminating in a rebuild on the reverted version plus credential rotation, forty minutes after the first message.

A dependency policy worth having

The policy exists to make everything above boring. It is one page, five bands, and every rule names the mechanism that enforces it — because a rule with no enforcement mechanism is a preference, and preferences do not survive a deadline.

Adopt. New direct dependencies go through Figure 7.1's gates, recorded in the PR description. Enforced by: a review checklist item that blocks merge on manifest changes. License. Permissive licenses are pre-approved; copyleft and unknown findings require a named decision-maker before merge; a notices file generates at build time. Enforced by: a CI license check against the lockfile that fails on new copyleft or unknown entries. Lock. Lockfiles are committed; every non-interactive install reads from them; regeneration happens in its own labeled PR. Enforced by: the lockfile-honoring install command in CI and the image build, plus a check that fails a PR where the lockfile changed without the manifest. Update. Bot-proposed bumps on the m8 cadence; patches auto-merge on green where tests justify it; majors become planned tickets reviewed at sprint planning. Enforced by: the bot's configuration and a standing planning agenda item. Respond. Security findings triage same day by severity × reachability × ships; accepted risks carry an owner and a 90-day expiry; the four bad days each have a named first move. Enforced by: an on-call rotation that owns the queue and a quarterly review of accepted exceptions.

Two closing anti-patterns, and they are twins. Policy theater is the twelve-page standard with a controls matrix that nobody has read past page two, which produces a document for auditors and no behavior change — worse than nothing, because it consumes the appetite for governance that a real policy needed. Policy absence is "use good judgment," which delegates every decision to whoever is most tired, in the moment they are least equipped, with no shared standard to appeal to. The design goal between them is an enforceable minimum: few enough rules that a new engineer reads all of them in five minutes, each one attached to a check that fails loudly. If a rule cannot be attached to a mechanism, either find the mechanism or cut the rule — an unenforceable rule on the page teaches the team that the page is decorative, which is the real cost.

The one-page dependency policy as five stacked bands — adopt, license, lock, update, respond — each with its rules and the CI check or ritual that enforces itOne page · five bands · every rule names its enforcementADOPTmodule 7new direct deps pass the five gates,recorded in the PR descriptioncheck: review item blocksmerge on manifest changeLICENSEmodule 3permissive pre-approved; copyleft orunknown needs a named decidercheck: CI license scan ofthe lockfile fails the buildLOCKmodules 4–5committed, installed from, regeneratedonly in a labeled PRcheck: lockfile-honoringinstall in CI and image buildUPDATEmodule 8patches batched weekly; majors becomeplanned tickets, never auto-mergedcheck: bot config +standing planning itemRESPONDmodule 9same-day triage; accepted risks carryan owner and a 90-day expirycheck: on-call owns the queue;quarterly exception reviewIf a rule cannot be attached to a mechanism, find the mechanism or cut the rule — an unenforceable rule teaches the team the page is decorative.
Figure 9.2 — The one-page policy. Five bands, each carrying two or three rules and the CI check or standing ritual that enforces them: adopt through the gates, decide licenses before merge, honor the lockfile on every install, update on a routed cadence, and respond with same-day triage and expiring exceptions. The test of the page is not its completeness but whether every line has a mechanism beside it.

Concept index

Dependency
A third-party package your code requires to build or run.
Direct dependency
A package you explicitly declared in your manifest.
Transitive dependency
A package pulled in by your dependencies' dependencies, recursively — chosen by strangers.
Dependency tree
The full resolved graph of direct and transitive packages an install produces.
Registry
The hosted index (npm, PyPI) that package managers resolve names and versions against.
Manifest
The human-edited file declaring direct dependencies and acceptable version ranges.
Maintainer
The person or group with commit and release authority over a package.
Contributor
Anyone whose changes a maintainer accepts; contribution confers no authority.
Governance model
Who decides what ships: solo maintainer, team, company-backed, or foundation-hosted.
Bus factor
How many people must disappear before a project cannot continue — often one.
Abandonment
The state where a package no longer receives fixes; the default fate of unfunded projects.
License
The conditional grant of rights without which you may not use the code at all.
Permissive license
A license (MIT, BSD, Apache-2.0) demanding little beyond attribution.
Copyleft
A license family (GPL) requiring conveyed derivative works to carry the same license.
Attribution
The obligation to preserve copyright and license notices — permissive licensing's real price.
Network copyleft (AGPL)
Copyleft whose obligations trigger on network use, closing the SaaS gap.
Provenance
Where code actually came from — the thing AI-suggested snippets lack by default.
Semantic versioning
The MAJOR.MINOR.PATCH promise protocol: breaking / additive / fix.
Breaking change
A change requiring downstream code to change; honest semver reserves it for majors.
Version range
A standing order (caret, tilde) auto-accepting future releases within a band.
Hyrum's law
With enough users, every observable behavior is depended on — so every change breaks someone.
Pinning
Specifying exact versions so nothing enters the build uninvited.
Dependency resolution
The solver run that turns manifest constraints plus registry state into one concrete tree.
Lockfile
The generated record of the exact resolved tree with integrity hashes — the source of truth for what ran.
Reproducible build
An install that yields the identical tree every time, everywhere — what lockfile discipline buys.
Version drift
Different resolution results from the same manifest at different times; 'works on my machine,' rebuilt.
Integrity hash
A checksum proving the fetched package byte-matches what the lockfile recorded.
CVE
A publicly catalogued vulnerability identifier — the unit of known-bad.
Typosquatting
Publishing malicious packages under near-miss names to catch fuzzy installs.
Dependency confusion
Shadowing an internal package name on a public registry so resolvers fetch the attacker's copy.
Supply-chain attack
Compromising software by compromising something it depends on rather than the target itself.
Install script
Code a package runs at install time, before you have executed a line of your own.
Blast radius
What breaks, and how badly, if a given dependency fails, lies, or leaves.
Vendoring
Copying a dependency's source into your repo — control and burden in one move.
Wrapper module
A thin in-house interface over a dependency that keeps it replaceable.
Update cadence
The deliberate rhythm at which reviewed dependency updates land.
Vulnerability scanning
Continuously matching your lockfile against advisory feeds — inventory times bad news.
SBOM
A machine-readable bill of materials for a built artifact; turns 'are we affected?' into a query.
Yanked release
A published version withdrawn by its maintainer or registry — one of the four bad days.
Dependency policy
The one-page, CI-enforced rules for adopting, licensing, locking, updating, and responding.

This is the complete text of the course. With JavaScript enabled, this same page runs the interactive edition — a self-diagnostic that reorders the syllabus around your gaps, knowledge checks, applied worksheets with model answers, and a spaced-repetition review queue — with progress saved locally in your browser.