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 => ({ 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 => ({ 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, 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) }) })