140 lines
5.4 KiB
TypeScript
140 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
|
import { assessLog, formatLog, repeatOffenders, type Deviation } from '../src/index.js'
|
|
|
|
const dev = (p: Partial<Deviation> = {}): Deviation => ({
|
|
id: 'd1',
|
|
procedure: 'septic-install',
|
|
step: 'step-7 backfill compaction',
|
|
operator: 'crew-a',
|
|
what: 'compacted in one lift instead of three',
|
|
where: 'work order 4412',
|
|
...p,
|
|
})
|
|
|
|
describe('a logged deviation is not a handled one', () => {
|
|
it('reports an unadjudicated deviation as not-assessed', () => {
|
|
const r = assessLog([dev()])
|
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
|
expect(r.findings[0]!.detail).toContain('an entry in a list, not a finding')
|
|
expect(overall(r)).toBe('not-assessed')
|
|
})
|
|
|
|
it('fails an adjudicated deviation with no action — the open loop', () => {
|
|
const r = assessLog([dev({ cause: 'procedure-defect' })])
|
|
expect(r.findings[0]!.determination).toBe('fail')
|
|
expect(r.findings[0]!.severity).toBe('critical')
|
|
expect(r.findings[0]!.detail).toContain('looks handled because someone wrote it down')
|
|
})
|
|
|
|
it('fails while the action is still open', () => {
|
|
const r = assessLog([dev({
|
|
cause: 'procedure-defect',
|
|
action: { description: 'rewrite step 7 for single-lift equipment', status: 'open' },
|
|
})])
|
|
expect(r.findings[0]!.determination).toBe('fail')
|
|
expect(r.findings[0]!.detail).toContain('the next crew meets the same step unchanged')
|
|
})
|
|
|
|
it('passes only when the action is applied', () => {
|
|
const r = assessLog([dev({
|
|
cause: 'procedure-defect',
|
|
action: { description: 'rewrite step 7', appliedTo: 'SOP-114 rev C', status: 'applied' },
|
|
})])
|
|
expect(overall(r)).toBe('pass')
|
|
expect(formatLog([dev({
|
|
cause: 'procedure-defect',
|
|
action: { description: 'rewrite step 7', appliedTo: 'SOP-114 rev C', status: 'applied' },
|
|
})], { evidence: 'full' })).toContain('SOP-114 rev C')
|
|
})
|
|
|
|
it('records a rejection as not-applicable and still prints it', () => {
|
|
// Printed so a rejection can be reviewed when the same step recurs.
|
|
const r = assessLog([dev({
|
|
cause: 'environment',
|
|
action: { description: 'no change — frozen ground is seasonal', status: 'rejected' },
|
|
})])
|
|
expect(r.findings[0]!.determination).toBe('not-applicable')
|
|
expect(formatLog([dev({
|
|
cause: 'environment',
|
|
action: { description: 'no change — frozen ground is seasonal', status: 'rejected' },
|
|
})])).toContain('frozen ground is seasonal')
|
|
})
|
|
})
|
|
|
|
describe('repeat deviations indict the procedure, not the operator', () => {
|
|
const closedAsError = (id: string, operator: string): Deviation => dev({
|
|
id,
|
|
operator,
|
|
cause: 'execution-error',
|
|
action: { description: 'retrained operator', status: 'applied' },
|
|
})
|
|
|
|
const repeated = [
|
|
closedAsError('d1', 'crew-a'),
|
|
closedAsError('d2', 'crew-b'),
|
|
closedAsError('d3', 'crew-c'),
|
|
]
|
|
|
|
it('counts DISTINCT operators, not incidents', () => {
|
|
// One person five times is a conversation. Five people once each is the step.
|
|
const sameGuy = [
|
|
closedAsError('d1', 'crew-a'),
|
|
closedAsError('d2', 'crew-a'),
|
|
closedAsError('d3', 'crew-a'),
|
|
]
|
|
expect(repeatOffenders(sameGuy)).toEqual([])
|
|
expect(repeatOffenders(repeated)).toHaveLength(1)
|
|
expect(repeatOffenders(repeated)[0]!.operators).toEqual(['crew-a', 'crew-b', 'crew-c'])
|
|
})
|
|
|
|
it('THE POINT: every incident closed clean, and the procedure is still indicted', () => {
|
|
// Each entry says "execution error, retrained, applied" — individually a
|
|
// pass. The aggregate is the only thing that sees the defect.
|
|
const r = assessLog(repeated)
|
|
expect(r.findings.filter(f => f.id.startsWith('repeat/'))).toHaveLength(1)
|
|
expect(overall(r)).toBe('fail')
|
|
const repeat = r.findings.find(f => f.id.startsWith('repeat/'))!
|
|
expect(repeat.severity).toBe('critical')
|
|
expect(repeat.detail).toContain('the mechanism by which a defective procedure survives')
|
|
})
|
|
|
|
it('leads with the aggregate, above the individual entries', () => {
|
|
const text = formatLog(repeated)
|
|
expect(text.indexOf('deviated from by 3 different operators'))
|
|
.toBeLessThan(text.indexOf('closed: retrained operator'))
|
|
})
|
|
|
|
it('does not group across different steps of the same procedure', () => {
|
|
expect(repeatOffenders([
|
|
dev({ id: 'd1', operator: 'a', step: 'step-7' }),
|
|
dev({ id: 'd2', operator: 'b', step: 'step-9' }),
|
|
])).toEqual([])
|
|
})
|
|
|
|
it('honours a custom operator threshold', () => {
|
|
expect(repeatOffenders(repeated, { minOperators: 4 })).toEqual([])
|
|
expect(repeatOffenders(repeated, { minOperators: 3 })).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('conformance with the evidence-record standard', () => {
|
|
it('an empty log is not-assessed, and says what empty means', () => {
|
|
const r = assessLog([])
|
|
expect(overall(r)).toBe('not-assessed')
|
|
expect(formatLog([])).toContain('evidence that none were written down')
|
|
})
|
|
|
|
it('every conclusion carries a citation', () => {
|
|
expect(validateReport(assessLog([
|
|
dev({ id: 'd1', cause: 'procedure-defect' }),
|
|
dev({ id: 'd2', operator: 'crew-b', cause: 'execution-error', action: { description: 'x', status: 'applied' } }),
|
|
]))).toEqual([])
|
|
})
|
|
|
|
it('cites the work order behind each deviation', () => {
|
|
expect(formatLog([dev({ cause: 'procedure-defect' })], { evidence: 'full' }))
|
|
.toContain('deviations#d1')
|
|
})
|
|
})
|