First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:49:00 -04:00
co-authored by Claude Opus 5.5
commit de1c6fb75c
14 changed files with 1873 additions and 0 deletions
+12
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
node_modules/
dist/
*.log
.DS_Store
.env*
!.env.example
.npmrc
dist-cjs/
+6
View File
@@ -0,0 +1,6 @@
stages: [test]
test:
stage: test
image: node:22-alpine
script: [npm ci, npm run build, npm test]
+14
View File
@@ -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.
+22
View File
@@ -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
```
+21
View File
@@ -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.
+79
View File
@@ -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).
+6
View File
@@ -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.
+1503
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -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"
}
}
BIN
View File
Binary file not shown.
+139
View File
@@ -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')
})
})
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist-cjs",
"declaration": false
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
"declaration": true, "outDir": "dist", "rootDir": "src",
"strict": true, "skipLibCheck": true
},
"include": ["src/**/*.ts"]
}