Files
build-or-buy/README.md
T
Paul HittandClaude Opus 5.5 2161af5ec0 Publish to the GitLab package registry on version tags
The README's install line failed: the package was never published, and
nothing told npm where the @extant2000 scope lives. A tag like v1.2.3 now
publishes to this project's registry with CI_JOB_TOKEN, and the README
sets the scope's registry before installing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:41:38 -04:00

3.7 KiB

BuildOrBuy

Compares building a capability against adopting one over a multi-year horizon, and checks the constraints that disqualify an option before it does any arithmetic.

What it does and why

Build-or-buy decisions are usually made as build days against a licence fee. That framing hides the three things that most often decide the answer, so this library models each of them.

A prohibited option is not an expensive one

Suppose a hosted vendor is cheaper, but its terms of service forbid the use you need. No cost comparison can give the right answer, because that option was never available. You record the constraint as a blocker. Blockers are checked before any cost is computed, and a blocked option is reported as not-applicable. Pricing it would imply it could win.

Self-hosting a third-party tool is a build with a buy label

Running someone else's software yourself is not purchasing it. You take on backups, upgrades, capacity and on-call, and none of that shows up in a licence table. An adopt-self-hosted option that has not listed its operationalSurface is reported as not assessed and left out of the ranking.

The operating cost dominates, and it is the number nobody fills in

Build cost is paid once and estimated with care. Operating cost is paid every year, for example the upkeep on an adopted tool or a usage bill that keeps running after the project that needed it has ended. Costs are totalled over horizonYears, and operateDaysPerYear: null makes an option uncostable instead of free. Treating that term as zero is how "adopt" wins on paper.

A recommendation made while any option is still unestimated is labelled provisional. When most of an option's cost is operating, the finding says so.

A cost win is not the whole decision

The result is a cost comparison, not a verdict. Some reasons to build are not about cost: data custody, keeping a third party out of the critical path, a licence you cannot accept. If such a reason rules an option out, model it as a blocker so the option is removed. If it is a preference, weigh it in the decision made around this output.

Usage

import { compare, formatComparison, type Candidate } from '@extant2000/build-or-buy'

const candidates: Candidate[] = [
  { name: 'in-house build', approach: 'build',
    costs: { buildDays: 20, operateDaysPerYear: 2 }, where: 'decision.md' },
  { name: 'hosted service', approach: 'buy-hosted',
    costs: { buildDays: 1, operateDaysPerYear: 1, cashPerYear: 600 }, where: 'decision.md' },
  { name: 'self-hosted tool', approach: 'adopt-self-hosted',
    costs: { buildDays: 3, operateDaysPerYear: 6 },
    operationalSurface: ['backups', 'upgrades', 'capacity', 'on-call'], where: 'decision.md' },
  { name: 'vendor X', approach: 'buy-hosted',
    costs: { buildDays: 1, operateDaysPerYear: 0, cashPerYear: 300 },
    blockers: [{ reason: 'terms of service prohibit the intended use', where: 'vendor-terms.md' }],
    where: 'decision.md' },
]

const cmp = { horizonYears: 3, dayRate: 800 }

const { winner, viable, disqualified, uncostable, reason } = compare(candidates, cmp)

// A report in the @extant2000/evidence-record format, rendered as text.
console.log(formatComparison(candidates, cmp))

totalCost(candidate, cmp) returns the horizon total, or null when an input is missing. toReport() returns the structured report that formatComparison() renders. Every finding cites the where of its candidate or blocker.

Install

The package is published to the GitLab package registry, so point the @extant2000 scope there first:

npm config set @extant2000:registry https://gitlab.com/api/v4/packages/npm/
npm install @extant2000/build-or-buy

License

MIT. See LICENSE.