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>
95 lines
3.7 KiB
Markdown
95 lines
3.7 KiB
Markdown
# 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
|
|
|
|
```ts
|
|
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](LICENSE).
|