First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:49:02 -04:00
co-authored by Claude Opus 5.5
commit 06c769b5d7
14 changed files with 2127 additions and 0 deletions
+12
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
*.log
.DS_Store
.env
.env.*
!.env.example
.npmrc
dist-cjs/
+6
View File
@@ -0,0 +1,6 @@
stages: [test]
test:
stage: test
image: node:22-alpine
script: [npm ci, npm run build, npm test]
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## 0.1.0 - 2026-09-28
First public release under MIT.
- `replan()` returns one of four decisions: `proceed`, `pivot`, `stop` or
`hold`.
- A plan with no recorded assumptions returns `hold`, never `proceed`.
- An assumption with `observed: null` is NOT ASSESSED and blocks.
- A broken assumption whose fallback has no `floor` makes the decision
`stop`.
- Non-numeric values are never ordered lexically; `equals` works on any
value.
- `toReport()` and `formatReplan()` produce an evidence-record report.
+21
View File
@@ -0,0 +1,21 @@
# Contributing
Issues and merge requests are welcome at
https://gitlab.com/extant2000/pivot-ops.
## Ground rules
- A test that cannot fail proves nothing. If you fix a bug, add a test and
check that it fails against the unfixed code before you submit.
- Measure, don't assume. Two modules with the same line count can still be
different programs.
- Keep dependencies minimal. Every new runtime dependency needs a reason.
- Explain why in comments, not what. The what is already in the code.
- A breaking change needs a major version bump and a note in CHANGELOG.md.
## Before you open a merge request
Run both of these and make sure they pass:
npm run build
npm test
+21
View File
@@ -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.
+108
View File
@@ -0,0 +1,108 @@
# PivotOps
Decide whether a plan still holds when conditions change, and name the
assumption that broke.
PivotOps is a pure function over a plan and its current readings. It has no
clock and does no I/O, and it executes nothing. It tells you whether to
proceed, pivot to a fallback, stop, or hold.
## What it checks and why
Conditions changing is not the real failure. The failure is a plan that never
wrote down what it assumed, so nobody can tell whether a change matters.
PivotOps is built against three versions of that.
1. A plan whose assumptions are implicit cannot be invalidated. Say a rollout
assumes a host has disk headroom, a registry is reachable and a
maintenance window is open. If none of that is recorded, a full disk only
shows up as the failure at the end. A plan with no recorded assumptions
returns `hold`, never `proceed`.
2. A stale reading is worse than no reading. Replanning against a condition
nobody re-measured gives a confident decision from an old fact. An
assumption with `observed: null` is NOT ASSESSED and blocks. "We did not
look" is not "it is fine".
3. Degradation without a floor is optimism. "Reduce concurrency and continue"
is a plan. "Continue at reduced capacity" with no point at which capacity
becomes unacceptable is a way of never stopping. A broken assumption whose
fallback has no `floor` makes the decision `stop`, not `pivot`.
## Four decisions, not two
| Decision | When |
|---|---|
| `proceed` | every assumption was re-checked and holds |
| `pivot` | something broke, and each broken assumption has a fallback with a floor |
| `stop` | something broke with no fallback, or with a fallback that has no floor |
| `hold` | nothing broke, but at least one condition was never re-measured |
`hold` gives "we do not know" its own outcome, separate from "carry on" and
"abandon". Folding it into either one leads to the two mistakes this library
exists to prevent: proceeding on unread conditions, or killing a viable
rollout because one probe was missing. A broken assumption outranks an unread
one, so `stop` wins over `hold`.
Non-numeric values are never ordered. An `at-least` comparison of `"closing"`
against `"open"` reports NOT ASSESSED instead of comparing the strings, which
would pass or fail on alphabetical order. `equals` works on any value.
## Usage
```ts
import { replan, formatReplan, type Plan } from '@extant2000/pivot-ops'
const plan: Plan = {
name: 'nightly rollout',
assumptions: [
{
id: 'disk-headroom',
description: 'the host has room for the new image',
condition: 'disk free bytes',
comparison: 'at-least',
required: 5e9,
observed: 2e9,
where: 'df -P /var/lib/docker',
fallback: { description: 'reduce concurrency to 1', floor: 'stop below 500 MB free' },
},
{
id: 'window',
description: 'the maintenance window is open',
condition: 'window state',
comparison: 'equals',
required: 'open',
observed: 'open',
where: 'change calendar',
},
],
}
const { decision, reason, broken, unverified } = replan(plan)
console.log(decision)
console.log(reason)
// The full report, for a terminal or CI log
console.log(formatReplan(plan))
```
`toReport(plan)` returns the same result as a `CapabilityReport` from
`@extant2000/evidence-record`, where every finding cites the reading it was
based on.
## Sample output
Produced by running the example above and printing `decision` and `reason`:
```
pivot
1 assumption(s) broken, each with a fallback and a floor: disk-headroom → reduce concurrency to 1.
```
## Install
```
npm install @extant2000/pivot-ops
```
## License
MIT. Copyright (c) 2026 Extant 2000 LLC. See [LICENSE](LICENSE).
+11
View File
@@ -0,0 +1,11 @@
# Security policy
## Reporting a vulnerability
Please report vulnerabilities privately by email to security@extant2000.com.
Do not open a public issue or merge request for a security problem.
Include the affected version, a description of the issue, and steps or a test
that reproduce it if you have them.
We aim to acknowledge every report within 5 business days.
+1504
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@extant2000/pivot-ops",
"version": "0.1.0",
"private": false,
"description": "Re-plans a deployment on environmental deviation, preserving OPSEC.",
"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/pivot-ops.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/pivot-ops",
"bugs": {
"url": "https://gitlab.com/extant2000/pivot-ops/-/issues"
}
}
+239
View File
@@ -0,0 +1,239 @@
/**
* PivotOps — decide whether a plan still holds when conditions move, and say
* which assumption broke.
*
* The failure is not that conditions changed. It is that a plan does not
* record what it assumed, so nobody can tell whether a change matters. Three
* shapes, all seen here:
*
* 1. **A plan whose assumptions are implicit cannot be invalidated.** A
* rollout assumed a host had disk headroom, a registry was reachable and
* a maintenance window was open. None of that was written down, so when
* the disk filled there was no mechanism to notice the plan was now
* wrong — only the failure at the end of it.
*
* 2. **A stale reading is worse than no reading.** Replanning against a
* condition nobody has re-measured produces a confident decision from
* an old fact. A condition with no observation is NOT ASSESSED, and it
* blocks — because "we did not look" is not "it is fine".
*
* 3. **Degradation without a floor is just optimism.** "Reduce concurrency
* and continue" is a plan; "continue at reduced capacity" without saying
* what capacity is unacceptable is a way of never stopping. A fallback
* without a floor is reported as a fallback that cannot fail.
*
* Pure: no clock, no I/O. It executes nothing.
*/
import {
type CapabilityReport,
type Finding,
type FormatOptions,
evidence,
formatReport as renderReport,
} from '@extant2000/evidence-record'
export type Comparison = 'at-least' | 'at-most' | 'equals'
export interface Assumption {
id: string
/** What the plan assumed, in a phrase. */
description: string
/** The condition being read, e.g. "disk free bytes". */
condition: string
comparison: Comparison
/** The value the plan needs. */
required: number | string
/**
* What was actually measured. `null` when the condition has not been
* re-read — never treated as satisfied.
*/
observed: number | string | null
/** Where the observation came from. */
where: string
/**
* What to do if this assumption fails. A fallback without `floor` is a
* fallback that cannot fail.
*/
fallback?: Fallback
}
export interface Fallback {
description: string
/**
* The point below which the fallback is no longer acceptable and the plan
* must stop instead of degrading further.
*/
floor?: string
}
function holds(a: Assumption): boolean | null {
if (a.observed === null) return null
if (a.comparison === 'equals') return a.observed === a.required
if (typeof a.observed !== 'number' || typeof a.required !== 'number') {
// A non-numeric value cannot be ordered. Reported as unknown rather than
// guessed — a string comparison here would silently pass or fail on
// lexical order, which is not what anyone meant.
return null
}
return a.comparison === 'at-least' ? a.observed >= a.required : a.observed <= a.required
}
function assumptionEvidence(a: Assumption) {
return typeof a.observed === 'number'
? evidence.measurement(a.condition, a.observed, 1, { note: a.where })
: evidence.file(a.where, undefined, String(a.observed ?? 'not observed'), a.condition)
}
function findingFor(a: Assumption): Finding {
const ok = holds(a)
const want = `${a.comparison.replace('-', ' ')} ${a.required}`
if (ok === null) {
return {
id: a.id,
summary: `${a.description} — ${a.condition} was not re-measured`,
determination: 'not-assessed',
severity: 'high',
detail: a.observed === null
? 'Replanning against a condition nobody re-read produces a confident decision from an old fact. "We did not look" is not "it is fine".'
: `"${a.observed}" cannot be ordered against "${a.required}". Reported as unknown rather than compared lexically, which is not what the plan meant.`,
evidence: [assumptionEvidence(a)],
}
}
if (ok) {
return {
id: a.id,
summary: `${a.description} — ${a.condition} is ${a.observed}, needs ${want}`,
determination: 'pass',
severity: 'info',
evidence: [assumptionEvidence(a)],
}
}
if (a.fallback && !a.fallback.floor) {
return {
id: a.id,
summary: `${a.description} BROKEN — fallback "${a.fallback.description}" has no floor`,
determination: 'fail',
severity: 'critical',
detail: 'A fallback without a floor is a fallback that cannot fail. State the point below which degrading further is unacceptable, or this is a way of never stopping.',
evidence: [assumptionEvidence(a)],
}
}
if (a.fallback) {
return {
id: a.id,
summary: `${a.description} BROKEN — falling back: ${a.fallback.description}`,
determination: 'fail',
severity: 'high',
detail: `${a.condition} is ${a.observed}, needs ${want}. Floor: ${a.fallback.floor}.`,
evidence: [assumptionEvidence(a)],
}
}
return {
id: a.id,
summary: `${a.description} BROKEN — no fallback defined`,
determination: 'fail',
severity: 'critical',
detail: `${a.condition} is ${a.observed}, needs ${want}. The plan has no alternative for this, so it stops here rather than proceeding on an assumption known to be false.`,
evidence: [assumptionEvidence(a)],
}
}
export interface Plan {
name: string
assumptions: Assumption[]
}
export type Decision = 'proceed' | 'pivot' | 'stop' | 'hold'
export interface Replan {
decision: Decision
reason: string
/** Assumptions that no longer hold. */
broken: Assumption[]
/** Assumptions nobody re-measured. */
unverified: Assumption[]
}
/**
* Decide what to do with a plan given the current readings.
*
* `hold` exists so that "we do not know" has an outcome distinct from both
* "carry on" and "abandon". Collapsing it into either is the decision this
* library is meant to prevent — proceeding on unread conditions, or stopping
* a viable rollout because one probe was missing.
*/
export function replan(plan: Plan): Replan {
const list = plan.assumptions ?? []
const broken = list.filter(a => holds(a) === false)
const unverified = list.filter(a => holds(a) === null)
if (list.length === 0) {
return {
decision: 'hold',
reason: 'The plan records no assumptions, so there is nothing to re-check. A plan that cannot be invalidated cannot be trusted to still hold.',
broken, unverified,
}
}
const unrecoverable = broken.filter(a => !a.fallback || !a.fallback.floor)
if (unrecoverable.length > 0) {
return {
decision: 'stop',
reason: `${unrecoverable.length} broken assumption(s) with no usable fallback: ${unrecoverable.map(a => a.id).join(', ')}.`,
broken, unverified,
}
}
if (broken.length > 0) {
return {
decision: 'pivot',
reason: `${broken.length} assumption(s) broken, each with a fallback and a floor: ${broken.map(a => `${a.id} → ${a.fallback!.description}`).join('; ')}.`,
broken, unverified,
}
}
if (unverified.length > 0) {
return {
decision: 'hold',
reason: `${unverified.length} assumption(s) unverified: ${unverified.map(a => a.id).join(', ')}. Proceeding would be a decision taken on unread conditions.`,
broken, unverified,
}
}
return { decision: 'proceed', reason: `All ${list.length} assumption(s) re-checked and holding.`, broken, unverified }
}
/** Report the plan's standing against current conditions. */
export function toReport(plan: Plan): CapabilityReport {
const list = plan.assumptions ?? []
const d = replan(plan)
if (list.length === 0) {
return {
capability: 'PivotOps',
scope: `${plan.name} — no assumptions recorded`,
examined: 0,
findings: [],
notes: [d.reason],
}
}
return {
capability: 'PivotOps',
scope: `${plan.name} — ${list.length} assumption(s)`,
examined: list.length,
findings: list.map(findingFor),
notes: [`decision: ${d.decision.toUpperCase()} — ${d.reason}`],
}
}
/** Render a replan for a terminal, CI log or operations review. */
export function formatReplan(plan: Plan, opts: FormatOptions = {}): string {
return renderReport(toReport(plan), opts)
}
+118
View File
@@ -0,0 +1,118 @@
import { describe, expect, it } from 'vitest'
import { overall, validateReport } from '@extant2000/evidence-record'
import { formatReplan, replan, toReport, type Assumption, type Plan } from '../src/index.js'
const disk = (p: Partial<Assumption> = {}): Assumption => ({
id: 'disk-headroom',
description: 'the host has room for the new image',
condition: 'disk free bytes',
comparison: 'at-least',
required: 5_000_000_000,
observed: 20_000_000_000,
where: 'df -P /var/lib/docker',
...p,
})
const plan = (assumptions: Assumption[]): Plan => ({ name: 'suite rollout', assumptions })
describe('a plan that records no assumptions cannot be invalidated', () => {
it('holds rather than proceeding', () => {
const d = replan(plan([]))
expect(d.decision).toBe('hold')
expect(d.reason).toContain('cannot be invalidated cannot be trusted')
expect(overall(toReport(plan([])))).toBe('not-assessed')
})
})
describe('unread is not fine', () => {
it('holds — not proceeds — when a condition was never re-measured', () => {
const d = replan(plan([disk(), disk({ id: 'registry', observed: null })]))
expect(d.decision).toBe('hold')
expect(d.reason).toContain('decision taken on unread conditions')
expect(d.unverified.map(a => a.id)).toEqual(['registry'])
})
it('reports the unread condition as not-assessed', () => {
const r = toReport(plan([disk({ observed: null })]))
expect(r.findings[0]!.determination).toBe('not-assessed')
expect(r.findings[0]!.detail).toContain('"We did not look" is not "it is fine"')
})
it('refuses to order non-numeric values lexically', () => {
// A string comparison would silently pass or fail on lexical order,
// which is not what the plan meant.
const r = toReport(plan([disk({
condition: 'window state', comparison: 'at-least',
required: 'open', observed: 'closing',
})]))
expect(r.findings[0]!.determination).toBe('not-assessed')
expect(r.findings[0]!.detail).toContain('rather than compared lexically')
})
it('compares equality on non-numeric values happily', () => {
const ok = disk({ condition: 'window', comparison: 'equals', required: 'open', observed: 'open' })
expect(replan(plan([ok])).decision).toBe('proceed')
})
})
describe('a fallback without a floor cannot fail', () => {
const broken = disk({ observed: 1_000_000_000 })
it('stops on a broken assumption whose fallback has no floor', () => {
const d = replan(plan([{ ...broken, fallback: { description: 'reduce concurrency and continue' } }]))
expect(d.decision).toBe('stop')
const r = toReport(plan([{ ...broken, fallback: { description: 'reduce concurrency and continue' } }]))
expect(r.findings[0]!.severity).toBe('critical')
expect(r.findings[0]!.detail).toContain('a way of never stopping')
})
it('pivots when the fallback states a floor', () => {
const d = replan(plan([{
...broken,
fallback: { description: 'reduce concurrency to 1', floor: 'stop below 500 MB free' },
}]))
expect(d.decision).toBe('pivot')
expect(d.reason).toContain('reduce concurrency to 1')
})
it('stops on a broken assumption with no fallback at all', () => {
const d = replan(plan([broken]))
expect(d.decision).toBe('stop')
expect(toReport(plan([broken])).findings[0]!.detail)
.toContain('rather than proceeding on an assumption known to be false')
})
it('a broken assumption outranks an unread one in the decision', () => {
// Stop is more urgent than hold.
const d = replan(plan([broken, disk({ id: 'x', observed: null })]))
expect(d.decision).toBe('stop')
})
})
describe('proceed', () => {
it('proceeds only when every assumption was re-checked and holds', () => {
const d = replan(plan([disk(), disk({ id: 'registry', condition: 'registry reachable', comparison: 'equals', required: 'yes', observed: 'yes' })]))
expect(d.decision).toBe('proceed')
expect(overall(toReport(plan([disk()])))).toBe('pass')
})
it('honours at-most comparisons', () => {
expect(replan(plan([disk({ comparison: 'at-most', required: 100, observed: 50 })])).decision).toBe('proceed')
expect(replan(plan([disk({ comparison: 'at-most', required: 100, observed: 150 })])).decision).toBe('stop')
})
})
describe('conformance with the evidence-record standard', () => {
it('prints the decision in the output', () => {
expect(formatReplan(plan([disk()]))).toContain('decision: PROCEED')
expect(formatReplan(plan([disk({ observed: 1 })]))).toContain('decision: STOP')
})
it('every conclusion carries a citation', () => {
expect(validateReport(toReport(plan([disk(), disk({ id: 'b', observed: 1 })])))).toEqual([])
})
it('carries the reading into the citation as a measurement', () => {
expect(formatReplan(plan([disk()]), { evidence: 'inline' })).toContain('disk free bytes = 20000000000')
})
})
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist-cjs",
"declaration": false
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
"declaration": true, "outDir": "dist", "rootDir": "src",
"strict": true, "skipLibCheck": true
},
"include": ["src/**/*.ts"]
}