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,9 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.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,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.
|
||||
@@ -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
|
||||
@@ -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,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
@@ -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.
|
||||
Generated
+1504
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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