268 lines
10 KiB
TypeScript
268 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
|
import {
|
|
assessTrl,
|
|
dominatedOptions,
|
|
formatSynthesis,
|
|
isMeasurable,
|
|
isUndiscriminated,
|
|
maturationPath,
|
|
nonDiscriminating,
|
|
requirementIssues,
|
|
supportedTrl,
|
|
synthesize,
|
|
unaddressedNeeds,
|
|
type MissionConcept,
|
|
type Need,
|
|
type Requirement,
|
|
} from '../src/index.js'
|
|
|
|
const need = (p: Partial<Need> = {}): Need => ({
|
|
id: 'N-1',
|
|
statement: 'the spacecraft must deorbit within 25 years of end of mission',
|
|
source: 'ODMSP 2019',
|
|
where: 'concept/needs.md',
|
|
...p,
|
|
})
|
|
|
|
const req = (p: Partial<Requirement> = {}): Requirement => ({
|
|
id: 'SYS-001',
|
|
statement: 'The system shall deorbit within 25 years',
|
|
derivedFrom: ['N-1'],
|
|
verification: { method: 'analysis', criterion: 'orbital decay analysis shows re-entry at less than 25 years' },
|
|
where: 'concept/requirements.md',
|
|
...p,
|
|
})
|
|
|
|
describe('derivation is checked in both directions', () => {
|
|
it('flags an invented requirement — the failure of a synthesised set', () => {
|
|
// Generating plausible requirements is easy; generating only warranted
|
|
// ones is not.
|
|
const issues = requirementIssues([req({ derivedFrom: [] })], [need()])
|
|
expect(issues.some(i => i.defect === 'invented')).toBe(true)
|
|
expect(issues.find(i => i.defect === 'invented')!.detail)
|
|
.toContain('nothing in the concept asked for it')
|
|
})
|
|
|
|
it('flags an unaddressed need — the failure of a hand-written set', () => {
|
|
// A downward-only check never sees this.
|
|
const orphanNeed = need({ id: 'N-2', statement: 'must survive launch loads' })
|
|
expect(unaddressedNeeds([req()], [need(), orphanNeed]).map(n => n.id)).toEqual(['N-2'])
|
|
})
|
|
|
|
it('rates an invented requirement critical and reports both directions together', () => {
|
|
const c: MissionConcept = {
|
|
name: 'test',
|
|
needs: [need(), need({ id: 'N-2', statement: 'survive launch' })],
|
|
requirements: [req(), req({ id: 'SYS-009', derivedFrom: [] })],
|
|
}
|
|
const r = synthesize(c)
|
|
expect(r.findings.some(f => f.id === 'req/SYS-009/invented')).toBe(true)
|
|
expect(r.findings.some(f => f.id === 'need/N-2')).toBe(true)
|
|
expect(overall(r)).toBe('fail')
|
|
})
|
|
|
|
it('catches a parent that does not exist — traceability that looks real', () => {
|
|
const issues = requirementIssues([req({ derivedFrom: ['N-99'] })], [need()])
|
|
const d = issues.find(i => i.defect === 'dangling-parent')!
|
|
expect(d.detail).toContain('it looks like traceability')
|
|
})
|
|
})
|
|
|
|
describe('a requirement nobody can fail is not a requirement', () => {
|
|
it('flags a missing verification method', () => {
|
|
const issues = requirementIssues([req({ verification: undefined })], [need()])
|
|
const u = issues.find(i => i.defect === 'unverifiable')!
|
|
expect(u.detail).toContain('there is nothing to fail')
|
|
})
|
|
|
|
it('rejects a criterion that describes a feeling', () => {
|
|
expect(isMeasurable('the interface shall be user-friendly')).toBe(false)
|
|
expect(isMeasurable('performance shall be acceptable')).toBe(false)
|
|
expect(isMeasurable('response time shall be less than 200 ms')).toBe(true)
|
|
expect(isMeasurable('the request shall be rejected')).toBe(true)
|
|
})
|
|
|
|
it('flags an immeasurable criterion', () => {
|
|
const issues = requirementIssues(
|
|
[req({ verification: { method: 'inspection', criterion: 'the design is robust' } })],
|
|
[need()],
|
|
)
|
|
expect(issues.some(i => i.defect === 'immeasurable')).toBe(true)
|
|
})
|
|
|
|
it('passes a properly derived, verifiable requirement', () => {
|
|
expect(requirementIssues([req()], [need()])).toEqual([])
|
|
expect(overall(synthesize({ name: 't', needs: [need()], requirements: [req()] }))).toBe('pass')
|
|
})
|
|
})
|
|
|
|
describe('TRL is capped by evidence, never asserted', () => {
|
|
it('REPRODUCES THE CLASSIC OVERCLAIM: TRL 6 backed by a unit test', () => {
|
|
// A unit test is a laboratory activity on a component. It supports 4.
|
|
const a = assessTrl(6, [
|
|
{ evidence: 'lab-bench', description: 'unit test suite passes', where: 'ci/unit-tests' },
|
|
])
|
|
expect(a.supported).toBe(3)
|
|
expect(a.effective).toBe(3)
|
|
expect(a.capped).toBe(true)
|
|
})
|
|
|
|
it('explains the cap by naming the environment each level requires', () => {
|
|
const c: MissionConcept = {
|
|
name: 'deorbit module', needs: [need()], requirements: [req()],
|
|
claimedTrl: 6,
|
|
maturity: [{ evidence: 'component-lab', description: 'thruster fired on the bench', where: 'test/TR-12' }],
|
|
}
|
|
const f = synthesize(c).findings.find(x => x.id === 'trl')!
|
|
expect(f.determination).toBe('fail')
|
|
expect(f.detail).toContain('an environment is a fact')
|
|
expect(f.detail).toContain('system or subsystem model demonstrated in a relevant environment')
|
|
})
|
|
|
|
it('takes the BEST evidence as the ceiling, not the worst', () => {
|
|
// Having also written a paper does not drag down a system demonstration.
|
|
expect(supportedTrl([
|
|
{ evidence: 'concept-paper', description: 'white paper', where: 'a' },
|
|
{ evidence: 'system-relevant-env', description: 'thermal vac campaign', where: 'b' },
|
|
])).toBe(6)
|
|
})
|
|
|
|
it('returns null for no evidence — not TRL 1', () => {
|
|
// TRL 1 is itself a claim that principles were observed and reported.
|
|
expect(supportedTrl([])).toBeNull()
|
|
const f = synthesize({
|
|
name: 't', needs: [need()], requirements: [req()], claimedTrl: 4,
|
|
}).findings.find(x => x.id === 'trl')!
|
|
expect(f.determination).toBe('not-assessed')
|
|
expect(f.detail).toContain('does not establish TRL 1 either')
|
|
})
|
|
|
|
it('accepts a claim the evidence supports', () => {
|
|
const a = assessTrl(5, [
|
|
{ evidence: 'component-relevant-env', description: 'vibe test at flight levels', where: 'TR-30' },
|
|
])
|
|
expect(a.capped).toBe(false)
|
|
expect(a.effective).toBe(5)
|
|
})
|
|
|
|
it('does not raise a modest claim to the evidence ceiling', () => {
|
|
const a = assessTrl(3, [{ evidence: 'qualified', description: 'full qual', where: 'x' }])
|
|
expect(a.effective).toBe(3)
|
|
expect(a.capped).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('the maturation path names demonstrations, not dates', () => {
|
|
it('produces one step per level, each with the evidence that raises it', () => {
|
|
const path = maturationPath(4, 6)
|
|
expect(path.map(s => s.to)).toEqual([5, 6])
|
|
expect(path[0]!.produces).toBe('component-relevant-env')
|
|
expect(path[1]!.produces).toBe('system-relevant-env')
|
|
expect(path[1]!.activity).toContain('relevant environment')
|
|
})
|
|
|
|
it('starts from zero when nothing has been demonstrated', () => {
|
|
expect(maturationPath(null, 3).map(s => s.to)).toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('is empty when the target is already met', () => {
|
|
expect(maturationPath(7, 6)).toEqual([])
|
|
})
|
|
|
|
it('is reported from the EFFECTIVE level, not the claimed one', () => {
|
|
const text = formatSynthesis({
|
|
name: 't', needs: [need()], requirements: [req()],
|
|
claimedTrl: 6, targetTrl: 7,
|
|
maturity: [{ evidence: 'lab-bench', description: 'unit tests', where: 'ci' }],
|
|
})
|
|
// effective is 3, so the path is 4..7 — four steps, not one.
|
|
expect(text).toContain('4 step(s)')
|
|
})
|
|
})
|
|
|
|
describe('a trade study with nothing to trade', () => {
|
|
const opt = (id: string, criteria: Record<string, string | number>, satisfies: string[] = ['SYS-001']) =>
|
|
({ id, name: `option ${id}`, satisfies, criteria, where: `trade/${id}.md` })
|
|
|
|
it('flags a single option as no trade study at all', () => {
|
|
const f = synthesize({
|
|
name: 't', needs: [need()], requirements: [req()],
|
|
options: [opt('A', { cost: 1 })],
|
|
}).findings.find(x => x.id === 'arch/single-option')!
|
|
expect(f.determination).toBe('not-assessed')
|
|
expect(f.detail).toContain('a decision already made')
|
|
})
|
|
|
|
it('CATCHES THE THEATRE: options identical on every criterion', () => {
|
|
// The table is full and the analysis is absent.
|
|
const options = [opt('A', { cost: 3, mass: 10 }), opt('B', { cost: 3, mass: 10 })]
|
|
expect(isUndiscriminated(options)).toBe(true)
|
|
const f = synthesize({ name: 't', needs: [need()], requirements: [req()], options })
|
|
.findings.find(x => x.id === 'arch/undiscriminated')!
|
|
expect(f.determination).toBe('fail')
|
|
expect(f.detail).toContain('which is why this survives review')
|
|
})
|
|
|
|
it('names the columns doing no work when only some are flat', () => {
|
|
const options = [opt('A', { cost: 3, mass: 10 }), opt('B', { cost: 5, mass: 10 })]
|
|
expect(nonDiscriminating(options)).toEqual(['mass'])
|
|
expect(isUndiscriminated(options)).toBe(false)
|
|
})
|
|
|
|
it('flags an option dominated on coverage', () => {
|
|
const options = [
|
|
opt('A', { cost: 1 }, ['SYS-001']),
|
|
opt('B', { cost: 2 }, ['SYS-001', 'SYS-002']),
|
|
]
|
|
const d = dominatedOptions(options)
|
|
expect(d).toHaveLength(1)
|
|
expect(d[0]!.dominated.id).toBe('A')
|
|
})
|
|
|
|
it('does not call an option dominated when it covers something unique', () => {
|
|
const options = [
|
|
opt('A', { cost: 1 }, ['SYS-003']),
|
|
opt('B', { cost: 2 }, ['SYS-001', 'SYS-002']),
|
|
]
|
|
expect(dominatedOptions(options)).toEqual([])
|
|
})
|
|
|
|
it('fails when no option satisfies a requirement', () => {
|
|
const f = synthesize({
|
|
name: 't', needs: [need()], requirements: [req(), req({ id: 'SYS-002' })],
|
|
options: [opt('A', { cost: 1 }, ['SYS-001']), opt('B', { cost: 2 }, ['SYS-001'])],
|
|
}).findings.find(x => x.id === 'arch/uncovered')!
|
|
expect(f.determination).toBe('fail')
|
|
expect(f.summary).toContain('SYS-002')
|
|
})
|
|
})
|
|
|
|
describe('conformance with the evidence-record standard', () => {
|
|
it('an empty concept is not-assessed, and says what empty means', () => {
|
|
const r = synthesize({ name: 'unnamed', needs: [], requirements: [] })
|
|
expect(overall(r)).toBe('not-assessed')
|
|
expect(formatSynthesis({ name: 'unnamed', needs: [], requirements: [] }))
|
|
.toContain('has not been synthesised, only named')
|
|
})
|
|
|
|
it('every conclusion carries a citation', () => {
|
|
expect(validateReport(synthesize({
|
|
name: 't',
|
|
needs: [need(), need({ id: 'N-2' })],
|
|
requirements: [req(), req({ id: 'SYS-009', derivedFrom: [] })],
|
|
claimedTrl: 6,
|
|
maturity: [{ evidence: 'lab-bench', description: 'unit tests', where: 'ci' }],
|
|
options: [
|
|
{ id: 'A', name: 'A', satisfies: ['SYS-001'], criteria: { cost: 1 }, where: 'a.md' },
|
|
{ id: 'B', name: 'B', satisfies: ['SYS-001'], criteria: { cost: 2 }, where: 'b.md' },
|
|
],
|
|
}))).toEqual([])
|
|
})
|
|
|
|
it('hands off to TraceProof on the requirement id', () => {
|
|
const r = synthesize({ name: 't', needs: [need()], requirements: [req()] })
|
|
expect(r.findings.some(f => f.id === 'req/SYS-001')).toBe(true)
|
|
})
|
|
})
|