@uwrl/qc-utils 0.0.22 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -35
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1710 -2184
- package/dist/index.umd.cjs +15 -15
- package/dist/types/index.d.ts +22 -0
- package/dist/utils/plotting/observation-record.d.ts +16 -45
- package/dist/utils/plotting/script.d.ts +7 -5
- package/package.json +1 -1
- package/dist/services/__tests__/createPatchObject.spec.d.ts +0 -1
- package/dist/services/__tests__/requestInterceptor.spec.d.ts +0 -1
- package/dist/services/__tests__/responseInterceptor.spec.d.ts +0 -1
- package/dist/services/api.d.ts +0 -147
- package/dist/services/apiMethods.d.ts +0 -8
- package/dist/services/createPatchObject.d.ts +0 -17
- package/dist/services/getCSRFToken.d.ts +0 -1
- package/dist/services/index.d.ts +0 -6
- package/dist/services/requestInterceptor.d.ts +0 -12
- package/dist/services/responseInterceptor.d.ts +0 -2
package/README.md
CHANGED
|
@@ -1,56 +1,263 @@
|
|
|
1
1
|
# @uwrl/qc-utils
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Worker-parallelized quality-control primitives for hydrological time-series.
|
|
4
|
+
Powers the QC pipeline in
|
|
5
|
+
[hydroserver-qc-app](https://github.com/hydroserver2/hydroserver-qc-app),
|
|
6
|
+
but the runtime has no Vue / app dependencies — anywhere you can run a
|
|
7
|
+
modern browser bundle is fair game.
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
The package wraps a paired `Float64Array` (timestamps, ms epoch) and
|
|
10
|
+
`Float32Array` (values) in an `ObservationRecord` and exposes a single
|
|
11
|
+
history-driven dispatch surface. Every edit and filter is logged as a
|
|
12
|
+
`HistoryItem` you can replay, undo / redo, calibrate against the host
|
|
13
|
+
machine, and serialize to disk as a JSON "QC script".
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
**1. From `qc-utils/`** (this repo):
|
|
15
|
+
## Install
|
|
10
16
|
|
|
11
17
|
```sh
|
|
12
|
-
npm install
|
|
13
|
-
npm link # registers @uwrl/qc-utils globally
|
|
14
|
-
npm run dev # starts vite build --watch — rebuilds dist/ on every source change
|
|
18
|
+
npm install @uwrl/qc-utils
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
## Quick start
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
ObservationRecord,
|
|
26
|
+
EnumFilterOperations,
|
|
27
|
+
EnumEditOperations,
|
|
28
|
+
Operator,
|
|
29
|
+
} from '@uwrl/qc-utils'
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
// Build a record from parallel datetime + value arrays.
|
|
32
|
+
const record = new ObservationRecord({
|
|
33
|
+
datetimes: [1704067200000, 1704067260000, 1704067320000, 1704067380000],
|
|
34
|
+
dataValues: [10.0, 11.5, 999.9, 12.3],
|
|
35
|
+
})
|
|
36
|
+
await record.reload()
|
|
37
|
+
|
|
38
|
+
// Find the spike, replace it with the previous value.
|
|
39
|
+
await record.dispatch([
|
|
40
|
+
[EnumFilterOperations.VALUE_THRESHOLD, { 'Greater than': 100 }],
|
|
41
|
+
[EnumEditOperations.CHANGE_VALUES, Operator.ASSIGN, 11.5],
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
record.dataY[2] // 11.5
|
|
45
|
+
record.history.length // 2
|
|
46
|
+
await record.undo() // replays without CHANGE_VALUES
|
|
47
|
+
record.dataY[2] // 999.9
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The dispatch chain is the canonical pattern: a filter (or explicit
|
|
51
|
+
`SELECTION`) seeds an index list, the next selection-consuming edit
|
|
52
|
+
reads it off `history[length - 2].selected`. See
|
|
53
|
+
[`docs/HISTORY_SCRIPT.md`](./docs/HISTORY_SCRIPT.md) for the full
|
|
54
|
+
operation-by-operation contract and the JSON wire format.
|
|
55
|
+
|
|
56
|
+
## Concepts
|
|
57
|
+
|
|
58
|
+
### `ObservationRecord`
|
|
59
|
+
|
|
60
|
+
The single state container. Holds:
|
|
61
|
+
|
|
62
|
+
- `dataX` / `dataY` — typed-array views into a (possibly shared) buffer.
|
|
63
|
+
- `history` — every committed `HistoryItem` since the last `reload()`.
|
|
64
|
+
- `redoStack` — items popped by `undo()`, ready for `redo()`.
|
|
65
|
+
|
|
66
|
+
Mutations only happen through `dispatch` / `dispatchAction` /
|
|
67
|
+
`dispatchFilter` / `undo` / `redo` / `reload` / `reloadHistory` /
|
|
68
|
+
`removeHistoryItem`. The handlers themselves are private — operations
|
|
69
|
+
are driven by enum + args so the same call shape works at runtime, on
|
|
70
|
+
replay from a saved script, and in unit tests.
|
|
71
|
+
|
|
72
|
+
### Edit operations (`EnumEditOperations`)
|
|
73
|
+
|
|
74
|
+
| Op | Purpose |
|
|
75
|
+
|-------------------------|----------------------------------------------------------|
|
|
76
|
+
| `ADD_POINTS` | Insert (datetime, value) tuples; reindex + sort by date. |
|
|
77
|
+
| `CHANGE_VALUES` | Apply `Operator` (ADD / SUB / MULT / DIV / ASSIGN) at the prior selection's indices. |
|
|
78
|
+
| `ASSIGN_VALUES_BULK` | Write parallel `values[i] → dataY[selection[i]]`. Table-driven edits. |
|
|
79
|
+
| `ASSIGN_DATETIMES_BULK` | Write parallel datetimes; runs as one combined delete + add. |
|
|
80
|
+
| `DELETE_POINTS` | Drop the selection from x / y in a single skip-on-delete pass. |
|
|
81
|
+
| `INTERPOLATE` | Linear interpolation across each consecutive group in the selection. |
|
|
82
|
+
| `SHIFT_DATETIMES` | Offset the selection's timestamps by `(amount, TimeUnit)`. |
|
|
83
|
+
| `DRIFT_CORRECTION` | Apply linear drift `value` to every consecutive group in the selection. |
|
|
84
|
+
| `FILL_GAPS` | Detect gaps over `gapThreshold`; insert points at `fillCadence` (interpolated or constant `fillValue`). |
|
|
85
|
+
|
|
86
|
+
### Filter operations (`EnumFilterOperations`)
|
|
87
|
+
|
|
88
|
+
All scan-style filters accept an optional trailing `[startTs, endTs]`
|
|
89
|
+
window in epoch ms; `DATETIME_RANGE`'s args ARE the window.
|
|
90
|
+
|
|
91
|
+
| Op | Args |
|
|
92
|
+
|-------------------|------------------------------------------------------------|
|
|
93
|
+
| `VALUE_THRESHOLD` | `[{ 'Greater than': n, 'Less than': n, ... }, range?]` |
|
|
94
|
+
| `DATETIME_RANGE` | `[fromTs?, toTs?]` |
|
|
95
|
+
| `CHANGE` | `[comparator, value, range?]` — Δ between adjacent points |
|
|
96
|
+
| `RATE_OF_CHANGE` | `[comparator, value, range?]` — value is a fraction (0.5 = 50%) |
|
|
97
|
+
| `FIND_GAPS` | `[amount, unit, range?]` |
|
|
98
|
+
| `PERSISTENCE` | `[times, range?]` — runs of identical repeated values |
|
|
99
|
+
| `SELECTION` | `[indices[]]` — explicit user selection |
|
|
100
|
+
|
|
101
|
+
### Worker dispatch + calibration
|
|
102
|
+
|
|
103
|
+
Every long-running kernel ships in two flavours: an inline core
|
|
104
|
+
(`changeValuesCore`, `fillGapsCore`, …) and a worker pool that scans
|
|
105
|
+
shared `Float64Array` / `Float32Array` views in parallel.
|
|
106
|
+
[`shouldUseWorker`](./src/utils/plotting/calibration.ts) picks per
|
|
107
|
+
call:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { ensureCalibration, shouldUseWorker, EnumEditOperations } from '@uwrl/qc-utils'
|
|
111
|
+
|
|
112
|
+
await ensureCalibration() // benchmark once per device, cached in localStorage
|
|
113
|
+
|
|
114
|
+
shouldUseWorker(EnumEditOperations.FILL_GAPS, {
|
|
115
|
+
datasetSize: record.dataX.length,
|
|
116
|
+
selectionSize: 0,
|
|
117
|
+
})
|
|
118
|
+
// → { useWorker: false, predictedInlineMs: 12.4, predictedWorkerMs: 53.0,
|
|
119
|
+
// reason: 'inline faster (12.4 vs 53.0 ms)' }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Workers require `SharedArrayBuffer`, which means the host page must
|
|
123
|
+
serve `Cross-Origin-Opener-Policy: same-origin` +
|
|
124
|
+
`Cross-Origin-Embedder-Policy: require-corp`. When SAB is unavailable
|
|
125
|
+
the dispatch transparently falls back to inline kernels. See
|
|
126
|
+
[`docs/CALIBRATION.md`](./docs/CALIBRATION.md) for the benchmark
|
|
127
|
+
methodology and the per-op cost table.
|
|
128
|
+
|
|
129
|
+
### QC scripts (save / load)
|
|
130
|
+
|
|
131
|
+
Every `ObservationRecord` history is round-trippable as JSON. The
|
|
132
|
+
on-disk shape IS the wire format used by the HydroServer API:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { serializeHistory, parseScript, applyScript } from '@uwrl/qc-utils'
|
|
136
|
+
|
|
137
|
+
const script = serializeHistory(record, {
|
|
138
|
+
startDate: '2024-01-01T00:00:00.000Z',
|
|
139
|
+
endDate: '2024-06-30T23:59:59.999Z',
|
|
140
|
+
})
|
|
141
|
+
// → { version: '1', createdAt, window, operations: [{ method, args }, ...] }
|
|
142
|
+
|
|
143
|
+
// On a fresh ObservationRecord with the same window's data loaded:
|
|
144
|
+
const fresh = new ObservationRecord(rawObservations)
|
|
145
|
+
await fresh.reload()
|
|
146
|
+
const report = await applyScript(fresh, parseScript(script))
|
|
147
|
+
report.applied // 12
|
|
148
|
+
report.failed // [{ index, method, error }] — per-op failures don't abort replay
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Scripts are reusable across datastreams: they don't pin a datastream id,
|
|
152
|
+
they store the wall-clock window and the `[method, ...args]` tuples.
|
|
153
|
+
See [`docs/HISTORY_SCRIPT.md`](./docs/HISTORY_SCRIPT.md) for versioning,
|
|
154
|
+
loader workflow, and per-op arg shape.
|
|
155
|
+
|
|
156
|
+
## Public API surface
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// State container
|
|
160
|
+
import { ObservationRecord, INCREASE_AMOUNT } from '@uwrl/qc-utils'
|
|
161
|
+
|
|
162
|
+
// Operation enums
|
|
163
|
+
import {
|
|
164
|
+
EnumEditOperations,
|
|
165
|
+
EnumFilterOperations,
|
|
166
|
+
Operator,
|
|
167
|
+
FilterOperation,
|
|
168
|
+
TimeUnit,
|
|
169
|
+
timeUnitMultipliers,
|
|
170
|
+
} from '@uwrl/qc-utils'
|
|
171
|
+
|
|
172
|
+
// QC scripts
|
|
173
|
+
import {
|
|
174
|
+
serializeHistory,
|
|
175
|
+
parseScript,
|
|
176
|
+
applyScript,
|
|
177
|
+
QcScript,
|
|
178
|
+
QcScriptOperation,
|
|
179
|
+
QcScriptWindow,
|
|
180
|
+
QC_SCRIPT_VERSION,
|
|
181
|
+
ApplyScriptReport,
|
|
182
|
+
} from '@uwrl/qc-utils'
|
|
183
|
+
|
|
184
|
+
// Calibration
|
|
185
|
+
import {
|
|
186
|
+
shouldUseWorker,
|
|
187
|
+
ensureCalibration,
|
|
188
|
+
runBenchmarks,
|
|
189
|
+
getCalibration,
|
|
190
|
+
onCalibrationChange,
|
|
191
|
+
clearCalibration,
|
|
192
|
+
DeviceProfile,
|
|
193
|
+
DispatchSignals,
|
|
194
|
+
DispatchDecision,
|
|
195
|
+
} from '@uwrl/qc-utils'
|
|
196
|
+
|
|
197
|
+
// Helpers
|
|
198
|
+
import {
|
|
199
|
+
findFirstGreaterOrEqual,
|
|
200
|
+
findLastLessOrEqual,
|
|
201
|
+
formatDate,
|
|
202
|
+
formatDuration,
|
|
203
|
+
measureEllapsedTime,
|
|
204
|
+
} from '@uwrl/qc-utils'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
A `Snackbar` notification helper is also exported for browser
|
|
208
|
+
consumers. It's the package's only DOM-touching symbol — the QC
|
|
209
|
+
engine itself is headless.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { Snackbar } from '@uwrl/qc-utils'
|
|
213
|
+
Snackbar.success('Saved')
|
|
25
214
|
```
|
|
26
215
|
|
|
27
|
-
|
|
216
|
+
For HydroServer REST calls, use `@hydroserver/client` directly. An
|
|
217
|
+
earlier `services/` REST client lived in this package and was
|
|
218
|
+
removed in `0.1.0` when the qc-app finished its migration to the
|
|
219
|
+
dedicated client.
|
|
28
220
|
|
|
29
|
-
|
|
221
|
+
## Browser requirements
|
|
30
222
|
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
-
|
|
223
|
+
- ES2022 / native `import`. Built as ESM with a CJS shim
|
|
224
|
+
(`dist/index.js` + `dist/index.cjs`).
|
|
225
|
+
- `SharedArrayBuffer` for the worker fast path (graceful inline fallback
|
|
226
|
+
when unavailable; see Calibration above).
|
|
227
|
+
- `Float64Array` / `Float32Array` typed-array `resize()` /
|
|
228
|
+
`SharedArrayBuffer.grow()` — Chrome 111+, Firefox 119+, Safari 16.4+.
|
|
34
229
|
|
|
35
|
-
##
|
|
230
|
+
## Contributing
|
|
36
231
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
| `npm run test` | Run the Vitest suite once. |
|
|
43
|
-
| `npm run coverage` | Run the suite with v8 coverage and the 80% threshold. |
|
|
44
|
-
| `npm run lint` | Run ESLint over `src/`. |
|
|
45
|
-
| `npm run lint:fix` | Auto-fix lintable errors. |
|
|
46
|
-
| `npm run preview` | Preview the production build with Vite. |
|
|
47
|
-
| `npm link` | Register this package for `npm link @uwrl/qc-utils` in consumers. |
|
|
48
|
-
| `npm publish` (`pub`) | Publish to npm under `--access public`. |
|
|
232
|
+
Clone, `npm install`, `npm run dev` (alias of `watch`) rebuilds
|
|
233
|
+
`dist/` on every source change so an `npm link`-ed consumer picks up
|
|
234
|
+
edits in ~1 s. The watch build skips `.d.ts` emit; run `npm run build`
|
|
235
|
+
once if the consumer surfaces stale type errors. CI runs
|
|
236
|
+
`tsc --noEmit → coverage → lint → build` on every push and PR to main.
|
|
49
237
|
|
|
50
|
-
|
|
238
|
+
| Script | Purpose |
|
|
239
|
+
|---------------------|--------------------------------------------------------|
|
|
240
|
+
| `npm run dev` | Watch-mode bundler for linked-dev workflow. |
|
|
241
|
+
| `npm run build` | Production build — bundle + emit `.d.ts` declarations. |
|
|
242
|
+
| `npm run test` | Vitest suite. |
|
|
243
|
+
| `npm run coverage` | Vitest with v8 coverage and the 80 % threshold. |
|
|
244
|
+
| `npm run lint` | ESLint over `src/`. |
|
|
245
|
+
|
|
246
|
+
Linking into a sibling `hydroserver-qc-app` checkout:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
# qc-utils (terminal 1)
|
|
250
|
+
npm link
|
|
251
|
+
npm run dev
|
|
252
|
+
|
|
253
|
+
# hydroserver-qc-app (terminal 2)
|
|
254
|
+
npm run link-qc-utils
|
|
255
|
+
npm run dev
|
|
256
|
+
```
|
|
51
257
|
|
|
52
|
-
|
|
258
|
+
HMR doesn't propagate through linked packages — refresh the consumer
|
|
259
|
+
browser to pick up changes.
|
|
53
260
|
|
|
54
261
|
## License
|
|
55
262
|
|
|
56
|
-
|
|
263
|
+
[BSD 3-Clause](./LICENSE).
|
package/dist/index.d.ts
CHANGED