First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env*
|
||||
!.env.example
|
||||
.npmrc
|
||||
dist-cjs/
|
||||
@@ -0,0 +1,6 @@
|
||||
stages: [test]
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: node:22-alpine
|
||||
script: [npm ci, npm run build, npm test]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0 - 2026-09-28
|
||||
|
||||
First public release under MIT.
|
||||
|
||||
- `assess()` returns a readiness of `transferable`, `blocked`, `unproven` or
|
||||
`unknown`, taken from the worst dependency, never the average.
|
||||
- Person and machine bindings with no substitute are critical blockers.
|
||||
Account bindings and unwritten knowledge with no substitute also block.
|
||||
- A substitute that nobody but its author has exercised is reported as not
|
||||
assessed.
|
||||
- `toReport()` and `formatHandOff()` produce and render a report in the
|
||||
`@extant2000/evidence-record` format.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Contributing
|
||||
|
||||
Issues and merge requests are welcome at
|
||||
https://gitlab.com/extant2000/hand-off.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **A test that cannot fail proves nothing.** If you fix a bug, add a test
|
||||
that fails without your fix, and check that it does fail before you submit.
|
||||
- Measure, do not assume. Two modules with the same line count can be
|
||||
different programs.
|
||||
- Keep dependencies minimal. Every new dependency needs a reason.
|
||||
- Explain why in comments, not what.
|
||||
|
||||
## Before you open a merge request
|
||||
|
||||
Run both of these and make sure they pass:
|
||||
|
||||
```
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Extant 2000 LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,82 @@
|
||||
# HandOff
|
||||
|
||||
Decides whether a capability can be operated by someone other than the
|
||||
person who built it, by looking at what it depends on.
|
||||
|
||||
## What it does and why
|
||||
|
||||
Handover is usually treated as a documentation problem. Documentation is the
|
||||
easy half. **Operability is the absence of dependence on a specific person or
|
||||
machine.** A thorough runbook does not remove a dependency. It describes one.
|
||||
|
||||
HandOff takes the list of things a capability depends on (credentials,
|
||||
runtimes, knowledge, access, data), what each one is bound to, and what the
|
||||
recipient would use instead. It checks them for three failures.
|
||||
|
||||
1. A personal credential doing institutional work. For example, several
|
||||
products deploy with one engineer's personal access token. No runbook
|
||||
transfers that: the recipient cannot obtain it, and it stops working when
|
||||
the account goes away. It is reported as a critical blocker, not a gap in
|
||||
the docs.
|
||||
2. A dependency on a machine instead of a role. A script that hard-codes a
|
||||
path on one laptop runs on that laptop. It works perfectly, forever, for
|
||||
one person.
|
||||
3. A substitute nobody has exercised. This one survives every review,
|
||||
because it is written down and so looks done. A runbook nobody but its
|
||||
author has followed is a draft, so it is reported as not assessed.
|
||||
|
||||
An account binding with no recorded way for the recipient to get their own
|
||||
account fails at high severity. Operating knowledge that exists only in
|
||||
someone's head fails too.
|
||||
|
||||
## Readiness is the worst dependency, never the average
|
||||
|
||||
One personal credential blocks a transfer however good everything else is.
|
||||
Averaging is how that blocker disappears behind nine green rows.
|
||||
|
||||
| Readiness | When |
|
||||
|---|---|
|
||||
| `transferable` | Every dependency is unbound or has a substitute someone else has exercised. |
|
||||
| `blocked` | At least one dependency cannot be handed over. |
|
||||
| `unproven` | Nothing is blocked, but at least one substitute has never been exercised. |
|
||||
| `unknown` | No dependencies were listed. The capability was assumed, not examined. |
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { assess, formatHandOff, type Capability } from '@extant2000/hand-off'
|
||||
|
||||
const capability: Capability = {
|
||||
name: 'release pipeline',
|
||||
recipient: 'the partner ops team',
|
||||
dependencies: [{
|
||||
name: 'deploy-token', kind: 'credential',
|
||||
coupling: 'person', boundTo: 'alice',
|
||||
substitute: 'a group deploy token per project',
|
||||
substituteVerified: true,
|
||||
where: 'config/deploy.env',
|
||||
}],
|
||||
}
|
||||
|
||||
const { readiness, reason, blockers, unproven } = assess(capability)
|
||||
console.log(formatHandOff(capability)) // report in the evidence-record format
|
||||
```
|
||||
|
||||
`substituteVerified` means someone other than the author has followed the
|
||||
substitute. Leaving it out counts as `false`, because an unexercised
|
||||
substitute is a claim. The example above is `transferable`; drop the
|
||||
substitute and it becomes `blocked`.
|
||||
|
||||
`coupling` is one of `person`, `machine`, `account` or `none`. `kind` is one
|
||||
of `credential`, `runtime`, `knowledge`, `access` or `data`. Every finding
|
||||
cites the dependency's `where`. The library is pure: no clock and no I/O.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @extant2000/hand-off
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,6 @@
|
||||
# Security
|
||||
|
||||
Please report vulnerabilities privately by email to security@extant2000.com.
|
||||
Do not open a public issue for a security problem.
|
||||
|
||||
We aim to acknowledge every report within 5 business days.
|
||||
Generated
+1503
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@extant2000/hand-off",
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"description": "Decides whether a capability can be operated by someone other than its builder, from what it depends on.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "./dist-cjs/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"dist-cjs",
|
||||
"src",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.com/extant2000/hand-off.git"
|
||||
},
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist-cjs/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist-cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@extant2000/evidence-record": "^0.1.2"
|
||||
},
|
||||
"author": "Extant 2000 LLC",
|
||||
"homepage": "https://gitlab.com/extant2000/hand-off",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/extant2000/hand-off/-/issues"
|
||||
}
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* HandOff — decide whether a capability can actually be operated by somebody
|
||||
* other than the person who built it.
|
||||
*
|
||||
* The mistake is treating this as a documentation problem. Documentation is
|
||||
* the easy half. **Operability is the absence of dependence on a specific
|
||||
* person or machine**, and a thorough runbook does not remove a dependency —
|
||||
* it describes one.
|
||||
*
|
||||
* Three failures:
|
||||
*
|
||||
* 1. **A personal credential doing institutional work.** Several products
|
||||
* authenticate with one engineer's personal access token. No runbook
|
||||
* transfers that; the receiving party cannot obtain it, and it dies
|
||||
* with the account. This is a blocker, not a documentation gap.
|
||||
*
|
||||
* 2. **A dependency on a machine rather than a role.** Scripts that
|
||||
* hard-code a path on one laptop run on exactly that laptop. They work
|
||||
* perfectly, forever, for one person.
|
||||
*
|
||||
* 3. **A substitute nobody has exercised.** A documented alternative that
|
||||
* has never been followed by anyone but its author is a claim. It reports
|
||||
* NOT ASSESSED here — a runbook nobody has followed is a draft.
|
||||
*
|
||||
* Readiness is the WORST dependency, never the average. One personal
|
||||
* credential blocks a transfer regardless of how good everything else is,
|
||||
* and averaging is how that disappears behind nine green rows.
|
||||
*
|
||||
* Pure: no clock, no I/O.
|
||||
*/
|
||||
|
||||
import {
|
||||
type CapabilityReport,
|
||||
type Finding,
|
||||
type FormatOptions,
|
||||
evidence,
|
||||
formatReport as renderReport,
|
||||
} from '@extant2000/evidence-record'
|
||||
|
||||
/** What a dependency is bound to, and therefore what blocks the transfer. */
|
||||
export type Coupling =
|
||||
/** Tied to an individual. Cannot be transferred, and dies with the account. */
|
||||
| 'person'
|
||||
/** Tied to one machine or environment. */
|
||||
| 'machine'
|
||||
/** Tied to an account that could in principle be re-issued to a role. */
|
||||
| 'account'
|
||||
/** Bound to nothing that has to change hands. */
|
||||
| 'none'
|
||||
|
||||
export type DependencyKind = 'credential' | 'runtime' | 'knowledge' | 'access' | 'data'
|
||||
|
||||
export interface Dependency {
|
||||
name: string
|
||||
kind: DependencyKind
|
||||
coupling: Coupling
|
||||
/** Who or what it is bound to — the person, the host, the account. */
|
||||
boundTo?: string
|
||||
/**
|
||||
* What the receiving party would use instead. A dependency with no
|
||||
* substitute is not transferable, however well documented it is.
|
||||
*/
|
||||
substitute?: string
|
||||
/**
|
||||
* Whether anyone OTHER than the author has exercised the substitute.
|
||||
* Undefined or false means it is a claim, not a capability.
|
||||
*/
|
||||
substituteVerified?: boolean
|
||||
/** Where the dependency lives — a file, a script, a runbook. */
|
||||
where: string
|
||||
}
|
||||
|
||||
export interface Capability {
|
||||
name: string
|
||||
/** The party expected to operate it after transfer. */
|
||||
recipient?: string
|
||||
dependencies: Dependency[]
|
||||
}
|
||||
|
||||
/** Couplings that must be broken before anyone else can operate the thing. */
|
||||
const BLOCKING: Coupling[] = ['person', 'machine']
|
||||
|
||||
function depEvidence(d: Dependency) {
|
||||
return evidence.file(d.where, undefined,
|
||||
`${d.kind}: ${d.coupling}${d.boundTo ? ` (${d.boundTo})` : ''}`,
|
||||
d.substitute ? `substitute: ${d.substitute}` : 'no substitute recorded')
|
||||
}
|
||||
|
||||
function findingFor(d: Dependency): Finding {
|
||||
const id = d.name
|
||||
const bound = d.boundTo ? ` to ${d.boundTo}` : ''
|
||||
|
||||
if (BLOCKING.includes(d.coupling) && !d.substitute) {
|
||||
return {
|
||||
id,
|
||||
summary: `${d.name} is bound${bound} and has no substitute`,
|
||||
determination: 'fail',
|
||||
severity: 'critical',
|
||||
detail: d.coupling === 'person'
|
||||
? 'A personal binding cannot be handed over. The receiving party cannot obtain it and it dies with the account — this is a blocker, not a documentation gap.'
|
||||
: 'This runs on one machine. It will keep working perfectly, forever, for one person.',
|
||||
evidence: [depEvidence(d)],
|
||||
}
|
||||
}
|
||||
|
||||
if (d.coupling === 'account' && !d.substitute) {
|
||||
return {
|
||||
id,
|
||||
summary: `${d.name} is bound to an account${bound} with no re-issue path recorded`,
|
||||
determination: 'fail',
|
||||
severity: 'high',
|
||||
detail: 'An account binding is transferable in principle, which is not the same as transferred. Record how the recipient obtains their own.',
|
||||
evidence: [depEvidence(d)],
|
||||
}
|
||||
}
|
||||
|
||||
if (d.kind === 'knowledge' && !d.substitute) {
|
||||
return {
|
||||
id,
|
||||
summary: `${d.name} exists only as knowledge, with nothing written down`,
|
||||
determination: 'fail',
|
||||
severity: 'high',
|
||||
detail: 'Undocumented operating knowledge transfers only by conversation, and only while the author is available.',
|
||||
evidence: [depEvidence(d)],
|
||||
}
|
||||
}
|
||||
|
||||
if (d.substitute && !d.substituteVerified) {
|
||||
return {
|
||||
id,
|
||||
summary: `${d.name} has a substitute nobody else has exercised`,
|
||||
determination: 'not-assessed',
|
||||
severity: 'high',
|
||||
// The failure that survives every review: it is written down, so it
|
||||
// looks done.
|
||||
detail: `"${d.substitute}" has not been followed by anyone but its author. A runbook nobody has followed is a draft — it is not evidence that the handover works.`,
|
||||
evidence: [depEvidence(d)],
|
||||
}
|
||||
}
|
||||
|
||||
if (d.substitute) {
|
||||
return {
|
||||
id,
|
||||
summary: `${d.name} — substitute exercised: ${d.substitute}`,
|
||||
determination: 'pass',
|
||||
severity: 'info',
|
||||
evidence: [depEvidence(d)],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
summary: `${d.name} is bound to nothing that has to change hands`,
|
||||
determination: 'pass',
|
||||
severity: 'info',
|
||||
evidence: [depEvidence(d)],
|
||||
}
|
||||
}
|
||||
|
||||
export type Readiness = 'transferable' | 'blocked' | 'unproven' | 'unknown'
|
||||
|
||||
export interface HandOffVerdict {
|
||||
readiness: Readiness
|
||||
reason: string
|
||||
/** Dependencies that block the transfer outright. */
|
||||
blockers: Dependency[]
|
||||
/** Substitutes that exist on paper and have never been exercised. */
|
||||
unproven: Dependency[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the capability can change hands.
|
||||
*
|
||||
* Computed from the WORST dependency, never the average: one personal
|
||||
* credential blocks a transfer regardless of how good everything else is,
|
||||
* and averaging is how that disappears behind nine green rows.
|
||||
*/
|
||||
export function assess(capability: Capability): HandOffVerdict {
|
||||
const deps = capability.dependencies ?? []
|
||||
|
||||
if (deps.length === 0) {
|
||||
return {
|
||||
readiness: 'unknown',
|
||||
reason: 'No dependencies were enumerated. A capability with no listed dependencies has not been examined — it has been assumed.',
|
||||
blockers: [], unproven: [],
|
||||
}
|
||||
}
|
||||
|
||||
const blockers = deps.filter(d =>
|
||||
(BLOCKING.includes(d.coupling) && !d.substitute)
|
||||
|| (d.coupling === 'account' && !d.substitute)
|
||||
|| (d.kind === 'knowledge' && !d.substitute))
|
||||
|
||||
const unproven = deps.filter(d => d.substitute && !d.substituteVerified)
|
||||
|
||||
if (blockers.length > 0) {
|
||||
return {
|
||||
readiness: 'blocked',
|
||||
reason: `${blockers.length} dependenc${blockers.length === 1 ? 'y' : 'ies'} cannot be handed over: ${blockers.map(d => d.name).join(', ')}.`,
|
||||
blockers, unproven,
|
||||
}
|
||||
}
|
||||
|
||||
if (unproven.length > 0) {
|
||||
return {
|
||||
readiness: 'unproven',
|
||||
reason: `${unproven.length} substitute(s) exist on paper and have never been exercised by anyone but the author: ${unproven.map(d => d.name).join(', ')}.`,
|
||||
blockers, unproven,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
readiness: 'transferable',
|
||||
reason: `All ${deps.length} dependenc${deps.length === 1 ? 'y is' : 'ies are'} either unbound or have an exercised substitute.`,
|
||||
blockers, unproven,
|
||||
}
|
||||
}
|
||||
|
||||
/** Report a capability's transfer readiness. */
|
||||
export function toReport(capability: Capability): CapabilityReport {
|
||||
const deps = capability.dependencies ?? []
|
||||
const v = assess(capability)
|
||||
|
||||
if (deps.length === 0) {
|
||||
return {
|
||||
capability: 'HandOff',
|
||||
scope: `${capability.name} — no dependencies enumerated`,
|
||||
examined: 0,
|
||||
findings: [],
|
||||
notes: [v.reason],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
capability: 'HandOff',
|
||||
scope: `${capability.name} → ${capability.recipient ?? 'an unnamed recipient'} — ${deps.length} dependenc${deps.length === 1 ? 'y' : 'ies'}`,
|
||||
examined: deps.length,
|
||||
findings: deps.map(findingFor),
|
||||
notes: [
|
||||
`readiness: ${v.readiness.toUpperCase()} — ${v.reason}`,
|
||||
'Readiness is the worst dependency, never the average — one personal credential blocks a transfer however good the rest is.',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/** Render a handover assessment for a terminal, a review or a transfer pack. */
|
||||
export function formatHandOff(capability: Capability, opts: FormatOptions = {}): string {
|
||||
return renderReport(toReport(capability), opts)
|
||||
}
|
||||
|
||||
export { BLOCKING }
|
||||
@@ -0,0 +1,126 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { overall, validateReport } from '@extant2000/evidence-record'
|
||||
import { assess, formatHandOff, toReport, type Capability, type Dependency } from '../src/index.js'
|
||||
|
||||
const dep = (p: Partial<Dependency> = {}): Dependency => ({
|
||||
name: 'deploy-token',
|
||||
kind: 'credential',
|
||||
coupling: 'none',
|
||||
where: 'config/deploy.env',
|
||||
...p,
|
||||
})
|
||||
|
||||
const cap = (dependencies: Dependency[]): Capability =>
|
||||
({ name: 'release pipeline', recipient: 'the partner ops team', dependencies })
|
||||
|
||||
describe('a personal credential is a blocker, not a documentation gap', () => {
|
||||
// Several products authenticating with one engineer's personal token.
|
||||
const personal = dep({ coupling: 'person', boundTo: 'alice', where: 'config/deploy.env' })
|
||||
|
||||
it('blocks regardless of how much is written down', () => {
|
||||
const v = assess(cap([personal]))
|
||||
expect(v.readiness).toBe('blocked')
|
||||
expect(v.blockers.map(d => d.name)).toEqual(['deploy-token'])
|
||||
})
|
||||
|
||||
it('says why documentation does not help', () => {
|
||||
const f = toReport(cap([personal])).findings[0]!
|
||||
expect(f.severity).toBe('critical')
|
||||
expect(f.detail).toContain('dies with the account')
|
||||
expect(f.detail).toContain('not a documentation gap')
|
||||
})
|
||||
|
||||
it('is satisfied by a recorded and exercised substitute', () => {
|
||||
const fixed = { ...personal, substitute: 'a group deploy token per project', substituteVerified: true }
|
||||
expect(assess(cap([fixed])).readiness).toBe('transferable')
|
||||
})
|
||||
|
||||
it('blocks a machine binding for the same reason', () => {
|
||||
// A script that hard-codes a path on one laptop runs on that laptop only.
|
||||
const machine = dep({ name: 'node-path', kind: 'runtime', coupling: 'machine', boundTo: 'dev-laptop-1', where: 'scripts/deploy.sh' })
|
||||
expect(assess(cap([machine])).readiness).toBe('blocked')
|
||||
expect(toReport(cap([machine])).findings[0]!.detail)
|
||||
.toContain('forever, for one person')
|
||||
})
|
||||
|
||||
it('treats an account binding as high, not critical — transferable in principle', () => {
|
||||
const acct = dep({ coupling: 'account', boundTo: 'ops@example.com' })
|
||||
const f = toReport(cap([acct])).findings[0]!
|
||||
expect(f.severity).toBe('high')
|
||||
expect(f.detail).toContain('which is not the same as transferred')
|
||||
})
|
||||
|
||||
it('blocks undocumented operating knowledge', () => {
|
||||
const k = dep({ name: 'restore-procedure', kind: 'knowledge', coupling: 'none' })
|
||||
expect(assess(cap([k])).readiness).toBe('blocked')
|
||||
expect(toReport(cap([k])).findings[0]!.detail).toContain('only while the author is available')
|
||||
})
|
||||
})
|
||||
|
||||
describe('a runbook nobody has followed is a draft', () => {
|
||||
const unexercised = dep({
|
||||
coupling: 'person', boundTo: 'alice',
|
||||
substitute: 'issue a group deploy token', substituteVerified: false,
|
||||
})
|
||||
|
||||
it('reports an unexercised substitute as not-assessed, never a pass', () => {
|
||||
// It is written down, so it looks done. That is why it survives review.
|
||||
const f = toReport(cap([unexercised])).findings[0]!
|
||||
expect(f.determination).toBe('not-assessed')
|
||||
expect(f.detail).toContain('A runbook nobody has followed is a draft')
|
||||
expect(overall(toReport(cap([unexercised])))).toBe('not-assessed')
|
||||
})
|
||||
|
||||
it('gives the capability its own readiness state', () => {
|
||||
const v = assess(cap([unexercised]))
|
||||
expect(v.readiness).toBe('unproven')
|
||||
expect(v.blockers).toHaveLength(0)
|
||||
expect(v.unproven).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('treats a missing substituteVerified the same as false', () => {
|
||||
const { substituteVerified, ...rest } = unexercised
|
||||
expect(assess(cap([rest as Dependency])).readiness).toBe('unproven')
|
||||
})
|
||||
})
|
||||
|
||||
describe('readiness is the worst dependency, never the average', () => {
|
||||
it('one blocker sinks nine clean dependencies', () => {
|
||||
const nineGood = Array.from({ length: 9 }, (_, i) =>
|
||||
dep({ name: `ok-${i}`, coupling: 'none' }))
|
||||
const v = assess(cap([...nineGood, dep({ name: 'pat', coupling: 'person', boundTo: 'alice' })]))
|
||||
expect(v.readiness).toBe('blocked')
|
||||
expect(formatHandOff(cap([...nineGood, dep({ name: 'pat', coupling: 'person' })])))
|
||||
.toContain('never the average')
|
||||
})
|
||||
|
||||
it('blocked outranks unproven', () => {
|
||||
const v = assess(cap([
|
||||
dep({ name: 'a', coupling: 'person' }),
|
||||
dep({ name: 'b', coupling: 'person', substitute: 'x' }),
|
||||
]))
|
||||
expect(v.readiness).toBe('blocked')
|
||||
expect(v.unproven).toHaveLength(1) // still reported
|
||||
})
|
||||
})
|
||||
|
||||
describe('conformance with the evidence-record standard', () => {
|
||||
it('no dependencies enumerated is unknown, never transferable', () => {
|
||||
const v = assess(cap([]))
|
||||
expect(v.readiness).toBe('unknown')
|
||||
expect(v.reason).toContain('has not been examined — it has been assumed')
|
||||
expect(overall(toReport(cap([])))).toBe('not-assessed')
|
||||
})
|
||||
|
||||
it('every conclusion carries a citation', () => {
|
||||
expect(validateReport(toReport(cap([
|
||||
dep({ name: 'a', coupling: 'person' }),
|
||||
dep({ name: 'b', coupling: 'none' }),
|
||||
])))).toEqual([])
|
||||
})
|
||||
|
||||
it('prints the readiness verdict', () => {
|
||||
expect(formatHandOff(cap([dep()]))).toContain('readiness: TRANSFERABLE')
|
||||
expect(formatHandOff(cap([dep({ coupling: 'person' })]))).toContain('readiness: BLOCKED')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist-cjs",
|
||||
"declaration": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
|
||||
"declaration": true, "outDir": "dist", "rootDir": "src",
|
||||
"strict": true, "skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user