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,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.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,14 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0 - 2026-09-28
|
||||
|
||||
First public release under MIT.
|
||||
|
||||
- `assessLog()` and `formatLog()` judge each deviation by the state of its
|
||||
loop: unadjudicated, no action, action open, rejected or applied.
|
||||
- An adjudicated deviation with no applied action is a failure, not a handled
|
||||
entry.
|
||||
- `repeatOffenders()` finds steps deviated from by several distinct
|
||||
operators. Those are reported as a critical procedure finding, above the
|
||||
individual entries, even when every incident was closed.
|
||||
- An empty log is reported as not assessed.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Contributing
|
||||
|
||||
Issues and merge requests are welcome at
|
||||
https://gitlab.com/extant2000/deviation-log.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **A test that cannot fail proves nothing.** If you fix a bug, add a test
|
||||
that fails without your fix, and check that it does fail before you submit.
|
||||
- Measure, do not assume. Two modules with the same line count can be
|
||||
different programs.
|
||||
- Keep dependencies minimal. Every new dependency needs a reason.
|
||||
- Explain why in comments, not what.
|
||||
|
||||
## 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,79 @@
|
||||
# DeviationLog
|
||||
|
||||
Turns field deviations from a written procedure into corrective actions, and
|
||||
does not count a logged deviation as a handled one.
|
||||
|
||||
## What it does and why
|
||||
|
||||
It is built against two failures.
|
||||
|
||||
### A logged deviation with no resulting action is an open loop
|
||||
|
||||
It looks handled because someone wrote it down, but nothing changed. A log
|
||||
that cannot tell recorded from resolved turns into an archive of problems
|
||||
nobody fixed. So each entry is judged by where its loop stands:
|
||||
|
||||
| State | Result |
|
||||
|---|---|
|
||||
| No cause decided (`unadjudicated`) | Not assessed. It is an entry in a list, not a finding yet. |
|
||||
| Cause decided, no action recorded | Fail. Critical when the cause is a procedure defect. |
|
||||
| Action still `open` | Fail. The next crew meets the same step unchanged. |
|
||||
| Action `rejected` | Not applicable, and still printed so the rejection can be reviewed if the step comes up again. |
|
||||
| Action `applied` | Pass, citing the document or procedure version it changed. |
|
||||
|
||||
### Repeat deviations point at the procedure, not the operator
|
||||
|
||||
When several different people depart from the same step, the common factor
|
||||
is the step. Judged one incident at a time, each looks like an individual
|
||||
mistake and gets closed with "retrained operator". Every entry passes, the
|
||||
step stays as written, and the deviations continue. Only the aggregate shows
|
||||
the defect, so DeviationLog computes it and lists it above the individual
|
||||
entries as a critical failure, even when every incident behind it was
|
||||
closed.
|
||||
|
||||
Repeats are grouped by procedure and step, and counted by distinct operator,
|
||||
not by incident. One person deviating five times is a conversation with that
|
||||
person. Five people deviating once each is a defective step. The default
|
||||
threshold is two operators, set with `minOperators`.
|
||||
|
||||
An empty log is reported as not assessed. It is not evidence that no
|
||||
deviations happened, only that none were written down.
|
||||
|
||||
The library is pure: no clock and no I/O. Reports use the
|
||||
`@extant2000/evidence-record` format, and every finding cites the record it
|
||||
came from.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { assessLog, formatLog, repeatOffenders, type Deviation } from '@extant2000/deviation-log'
|
||||
|
||||
const deviations: Deviation[] = [
|
||||
{ id: 'd1', procedure: 'pump-install', step: 'step 7: torque check', operator: 'crew-a',
|
||||
what: 'skipped the second torque pass', cause: 'execution-error',
|
||||
action: { description: 'retrained operator', status: 'applied' }, where: 'work order 101' },
|
||||
{ id: 'd2', procedure: 'pump-install', step: 'step 7: torque check', operator: 'crew-b',
|
||||
what: 'skipped the second torque pass', cause: 'execution-error',
|
||||
action: { description: 'retrained operator', status: 'applied' }, where: 'work order 102' },
|
||||
{ id: 'd3', procedure: 'pump-install', step: 'step 9: leak test', operator: 'crew-c',
|
||||
what: 'tested at half pressure', where: 'work order 103' },
|
||||
]
|
||||
|
||||
const groups = repeatOffenders(deviations) // steps hit by 2+ operators
|
||||
const report = assessLog(deviations, { minOperators: 2 })
|
||||
console.log(formatLog(deviations))
|
||||
```
|
||||
|
||||
Here both step 7 incidents pass on their own, yet the report fails: step 7
|
||||
was deviated from by two different operators, and that repeat finding comes
|
||||
first. The step 9 entry is not assessed because no cause has been decided.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @extant2000/deviation-log
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,6 @@
|
||||
# Security
|
||||
|
||||
Please report vulnerabilities privately by email to security@extant2000.com.
|
||||
Do not open a public issue for a security problem.
|
||||
|
||||
We aim to acknowledge every report within 5 business days.
|
||||
Generated
+1503
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@extant2000/deviation-log",
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"description": "Turns procedure deviations into corrective actions and flags steps that several operators deviate from.",
|
||||
"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/deviation-log.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/deviation-log",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/extant2000/deviation-log/-/issues"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,139 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -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