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