diagcalc 3.2.3 → 5.0.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 +60 -6
- package/bin/diagcalc.js +147 -41
- package/docs/PROVENANCE.md +17 -0
- package/index.html +408 -92
- package/lib/README.md +98 -0
- package/lib/diagcalc-case.js +148 -0
- package/lib/diagcalc-core.js +557 -89
- package/lib/diagcalc-datasets.js +140 -1
- package/lib/diagcalc-geometry.js +27 -0
- package/lib/diagcalc-i18n.js +255 -0
- package/lib/diagcalc-meta.js +9 -0
- package/lib/diagcalc-presentation.js +278 -0
- package/lib/diagcalc-storage.js +45 -0
- package/lib/diagcalc-types.d.ts +59 -0
- package/package.json +18 -4
- package/script.js +1239 -275
- package/styles.css +2186 -325
- package/tui/index.js +83 -273
- package/tui/input.js +24 -0
- package/tui/presentation.js +168 -0
- package/tui/report.js +130 -0
- package/web/charts.js +339 -0
- package/web/results.js +234 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Both interfaces use the same calculation engine.
|
|
|
10
10
|
## Repository
|
|
11
11
|
|
|
12
12
|
- GitHub: `https://github.com/tiagojct/diagcalc`
|
|
13
|
-
- Web app: `https://diagcalc.
|
|
13
|
+
- Web app: `https://diagcalc.tiagojacinto.eu/`
|
|
14
14
|
- npm package: `https://www.npmjs.com/package/diagcalc`
|
|
15
15
|
|
|
16
16
|
## Description
|
|
@@ -47,11 +47,28 @@ The web and terminal interfaces both follow that workflow.
|
|
|
47
47
|
- sensitivity, specificity, PPV, NPV
|
|
48
48
|
- LR+ and LR-
|
|
49
49
|
- positive and negative post-test probability
|
|
50
|
-
- 95%
|
|
51
|
-
-
|
|
50
|
+
- Wilson 95% intervals for proportions and log-normal intervals for likelihood ratios and DOR
|
|
51
|
+
- illustrative teaching scenarios with explicit provenance
|
|
52
52
|
- Fagan nomogram in the web app
|
|
53
53
|
- TUI, text CLI, and JSON CLI output in the terminal app
|
|
54
54
|
|
|
55
|
+
## Case Studies
|
|
56
|
+
|
|
57
|
+
All current presets are illustrative teaching scenarios. Medical references provide background; their confusion-matrix counts have not been verified against retained source-table extractions. They must not be presented as published study estimates.
|
|
58
|
+
|
|
59
|
+
- `screening` - low-prevalence population screening workflow
|
|
60
|
+
- `caseControl` - balanced case-control teaching example
|
|
61
|
+
- `clinic` - specialist clinic setting with intermediate prevalence
|
|
62
|
+
- `ddimer` - D-dimer for pulmonary embolism rule-out
|
|
63
|
+
- `troponin` - high-sensitivity troponin for acute myocardial infarction
|
|
64
|
+
- `mammography` - screening mammography in a low-prevalence setting
|
|
65
|
+
- `covid_antigen` - rapid antigen testing for COVID-19
|
|
66
|
+
- `hiv_elisa` - fourth-generation HIV ELISA screening
|
|
67
|
+
- `strep_throat` - rapid antigen testing for streptococcal pharyngitis
|
|
68
|
+
- `xray_pneumonia` - chest X-ray for community-acquired pneumonia
|
|
69
|
+
|
|
70
|
+
These cases are included to support teaching across different prevalence settings, screening vs. confirmation logic, and Bayesian interpretation of test results.
|
|
71
|
+
|
|
55
72
|
## Web App
|
|
56
73
|
|
|
57
74
|
The web app is static. It does not need a build step.
|
|
@@ -82,7 +99,7 @@ http://localhost:8080
|
|
|
82
99
|
|
|
83
100
|
## Terminal App
|
|
84
101
|
|
|
85
|
-
The terminal app requires Node.js
|
|
102
|
+
The terminal app requires Node.js 22 or newer.
|
|
86
103
|
|
|
87
104
|
It is intended for keyboard-first use, quick calculations, reproducible terminal workflows, and scripting.
|
|
88
105
|
|
|
@@ -148,6 +165,38 @@ The CLI is useful when you want a one-shot calculation or when you want to integ
|
|
|
148
165
|
- `r`: reset current case
|
|
149
166
|
- `q`: quit
|
|
150
167
|
|
|
168
|
+
## Validation and reproducibility
|
|
169
|
+
|
|
170
|
+
Counts must be non-negative safe integers, including their total. Both disease cohorts must be present. Pre-test probability accepts complete dot/comma decimal strings from 0 through 100 inclusive. Impossible conditioning events remain undefined and display an em dash; infinite ratios display infinity. Chaining retains full precision and assumes tests are conditionally independent given disease status.
|
|
171
|
+
|
|
172
|
+
The browser's history retains the inputs, continuity correction, engine version and origin for each calculation. Editing a preset marks it customised and invalidates previous results. Storage restrictions fall back to session memory; they do not prevent calculations. Older history entries are explicitly marked as recalculated legacy cases because their original correction settings were not recorded.
|
|
173
|
+
|
|
174
|
+
### Version 5 JSON compatibility
|
|
175
|
+
|
|
176
|
+
`--format json` now emits schema version 2. Every metric and CI endpoint encodes exceptional numbers explicitly:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{"value": null, "status": "infinite", "ci": null}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Statuses are `finite`, `infinite`, `negative-infinite`, and `undefined`; finite values are unrounded. Consumers of the previous JSON format must migrate. Outputs include engine version, inputs, correction options and provenance. TUI text/Markdown exports append a reproducible JSON snapshot and refuse to overwrite existing paths.
|
|
183
|
+
|
|
184
|
+
ROC rows must represent the same cohorts with monotonic operating points and consistently ordered cutoffs. Synthetic points and edited synthetic points remain labelled illustrative, including their AUC. Decision thresholds are unavailable for uninformative or inverted tests. Nomogram lines outside its labelled axis range are explicitly omitted; numeric probabilities remain available.
|
|
185
|
+
|
|
186
|
+
## Development checks
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npm ci
|
|
190
|
+
npx playwright install chromium firefox webkit
|
|
191
|
+
npm run check
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`check` runs JavaScript type checking, engine/CLI/TUI/package tests and browser tests in Chromium, Firefox and WebKit. Browser tests prepare the actual static deployment bundle automatically. Python 3 and a pseudo-terminal are required for the real terminal test on Unix; that test is skipped on Windows. There are no runtime browser dependencies or compilation step.
|
|
195
|
+
|
|
196
|
+
Pull requests run checks on Node 22 and 24 and all three browser engines. Main-branch deployment requires both verification jobs. `npm run prepare:site` copies only browser assets and `CNAME` into `public/`; package tests also extract and execute the actual npm archive outside the checkout.
|
|
197
|
+
|
|
198
|
+
See [lib/README.md](lib/README.md) for API contracts and [docs/PROVENANCE.md](docs/PROVENANCE.md) for dataset evidence requirements.
|
|
199
|
+
|
|
151
200
|
## Deployment
|
|
152
201
|
|
|
153
202
|
### GitHub Pages
|
|
@@ -187,8 +236,13 @@ Package page:
|
|
|
187
236
|
|
|
188
237
|
- `index.html` - web app markup
|
|
189
238
|
- `styles.css` - web app styles
|
|
190
|
-
- `script.js` - web
|
|
191
|
-
- `
|
|
239
|
+
- `script.js` - web state and event handling
|
|
240
|
+
- `web/` - result rendering and scheduled DPR-aware charts
|
|
241
|
+
- `lib/diagcalc-core.js` - shared numeric calculations and validation
|
|
242
|
+
- `lib/diagcalc-presentation.js` - English/Portuguese metric labels and interpretation
|
|
243
|
+
- `lib/diagcalc-case.js` - versioned snapshots and safe exceptional-number encoding
|
|
244
|
+
- `lib/diagcalc-storage.js` - resilient browser preferences and session fallback
|
|
245
|
+
- `lib/diagcalc-geometry.js` - independently testable nomogram geometry
|
|
192
246
|
- `lib/diagcalc-datasets.js` - shared preset datasets
|
|
193
247
|
- `tui/index.js` - terminal UI
|
|
194
248
|
- `bin/diagcalc.js` - CLI and TUI entrypoint
|
package/bin/diagcalc.js
CHANGED
|
@@ -2,32 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
const core = require("../lib/diagcalc-core");
|
|
4
4
|
const datasetStore = require("../lib/diagcalc-datasets");
|
|
5
|
+
const pkg = require("../package.json");
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const args = {
|
|
8
|
-
raw: [],
|
|
9
|
-
};
|
|
7
|
+
const caseStore = require("../lib/diagcalc-case");
|
|
10
8
|
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const flags = new Set(["help", "tui", "chain", "list-datasets"]);
|
|
11
|
+
const values = new Set(["dataset", "tp", "fp", "fn", "tn", "pre", "tp2", "fp2", "fn2", "tn2", "chain-from", "format", "continuity"]);
|
|
12
|
+
const args = {};
|
|
11
13
|
for (let index = 0; index < argv.length; index += 1) {
|
|
12
14
|
const token = argv[index];
|
|
13
|
-
if (!token.startsWith("--")) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const next = argv[index + 1];
|
|
20
|
-
const isFlag = !next || next.startsWith("--");
|
|
21
|
-
|
|
22
|
-
if (isFlag) {
|
|
15
|
+
if (!token.startsWith("--")) return { error: `Unexpected argument: ${token}` };
|
|
16
|
+
const [key, ...parts] = token.slice(2).split("=");
|
|
17
|
+
if (!flags.has(key) && !values.has(key)) return { error: `Unknown option: --${key}` };
|
|
18
|
+
if (Object.prototype.hasOwnProperty.call(args, key)) return { error: `Duplicate option: --${key}` };
|
|
19
|
+
if (flags.has(key)) {
|
|
20
|
+
if (parts.length) return { error: `--${key} does not take a value.` };
|
|
23
21
|
args[key] = true;
|
|
24
22
|
continue;
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const value = parts.length ? parts.join("=") : argv[++index];
|
|
25
|
+
if (value === undefined || value === "" || value.startsWith("--")) return { error: `Missing value for --${key}.` };
|
|
26
|
+
args[key] = value;
|
|
27
|
+
}
|
|
28
|
+
if (!args.chain && ["tp2", "fp2", "fn2", "tn2", "chain-from"].some((key) => key in args)) {
|
|
29
|
+
return { error: "Second-test options require --chain." };
|
|
29
30
|
}
|
|
30
|
-
|
|
31
31
|
return args;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -40,6 +40,7 @@ function printHelp() {
|
|
|
40
40
|
" diag --dataset hiv_elisa",
|
|
41
41
|
" diag --tp 199 --fp 1 --fn 1 --tn 9799 --pre 2",
|
|
42
42
|
" diag --dataset ddimer --pre 18",
|
|
43
|
+
" diag --dataset ddimer --chain --tp2 90 --fp2 10 --fn2 10 --tn2 90",
|
|
43
44
|
" diag --list-datasets",
|
|
44
45
|
"",
|
|
45
46
|
"Options:",
|
|
@@ -50,7 +51,11 @@ function printHelp() {
|
|
|
50
51
|
" --fn <n> False negatives",
|
|
51
52
|
" --tn <n> True negatives",
|
|
52
53
|
" --pre <n> Pre-test probability (%)",
|
|
54
|
+
" --chain Compute a second test using test 1's post-test as pre-test",
|
|
55
|
+
" --tp2 --fp2 --fn2 --tn2 Second test's confusion matrix (with --chain)",
|
|
56
|
+
" --chain-from <r> positive (default) | negative — which test 1 result to follow",
|
|
53
57
|
" --format <type> Output format: text or json",
|
|
58
|
+
" --continuity <m> Continuity correction for LR/DOR CIs: auto (default), always, never",
|
|
54
59
|
" --list-datasets Show available dataset keys",
|
|
55
60
|
" --help Show this help message",
|
|
56
61
|
"",
|
|
@@ -60,24 +65,32 @@ function printHelp() {
|
|
|
60
65
|
].join("\n"));
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function printJsonReport(dataset, input, metrics) {
|
|
68
|
+
function printJsonReport(dataset, input, metrics, chained, warnings, continuity) {
|
|
69
|
+
const modified = Boolean(dataset && ["tp", "fp", "fn", "tn", "preTestProb"].some((key) => input[key] !== dataset[key]));
|
|
70
|
+
const snapshot = caseStore.createSnapshot(input, { continuityCorrection: continuity }, {
|
|
71
|
+
label: dataset ? `${modified ? "Customised: " : ""}${dataset.name}` : "Ad hoc case",
|
|
72
|
+
datasetKey: dataset ? dataset.key : null,
|
|
73
|
+
modified,
|
|
74
|
+
provenance: dataset ? dataset.provenance : null,
|
|
75
|
+
});
|
|
74
76
|
const payload = {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
...snapshot,
|
|
78
|
+
engine: { name: "diagcalc", version: pkg.version, generatedAt: snapshot.savedAt, ciMethods: caseStore.methodMetadata(snapshot.options) },
|
|
79
|
+
case: snapshot.label,
|
|
80
|
+
warnings,
|
|
81
|
+
metrics: Object.fromEntries(Object.entries(metrics).map(([key, metric]) => [key, {
|
|
82
|
+
...snapshot.metrics[key], label: metric.label,
|
|
83
|
+
formatted: core.formatValue(metric.value, metric.formatter), note: metric.note,
|
|
84
|
+
}])),
|
|
79
85
|
};
|
|
80
|
-
|
|
86
|
+
if (chained) {
|
|
87
|
+
payload.chained = {
|
|
88
|
+
...caseStore.createSnapshot(chained.input, { continuityCorrection: continuity }, { label: "Chained second test" }),
|
|
89
|
+
from: chained.from,
|
|
90
|
+
assumption: "Test results are conditionally independent given disease status.",
|
|
91
|
+
warnings: core.buildBiasWarnings(chained.input),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
81
94
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
82
95
|
}
|
|
83
96
|
|
|
@@ -115,8 +128,7 @@ function buildInputFromArgs(args) {
|
|
|
115
128
|
merged.tn = core.safeParseInt(args.tn);
|
|
116
129
|
}
|
|
117
130
|
if (typeof args.pre === "string") {
|
|
118
|
-
|
|
119
|
-
merged.preTestProb = normalised === "" ? NaN : parseFloat(normalised);
|
|
131
|
+
merged.preTestProb = core.parseProbability(args.pre);
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
return {
|
|
@@ -128,17 +140,19 @@ function buildInputFromArgs(args) {
|
|
|
128
140
|
function renderMetricLine(metric) {
|
|
129
141
|
const value = core.formatValue(metric.value, metric.formatter);
|
|
130
142
|
const ci = metric.ci
|
|
131
|
-
? ` | 95% CI ${core.
|
|
143
|
+
? ` | 95% CI ${core.formatValue(metric.ci.lower, metric.formatter)} to ${core.formatValue(metric.ci.upper, metric.formatter)}`
|
|
132
144
|
: "";
|
|
133
145
|
return `${metric.label}: ${value}${ci}`;
|
|
134
146
|
}
|
|
135
147
|
|
|
136
|
-
function printReport(dataset, metrics) {
|
|
148
|
+
function printReport(dataset, metrics, warnings) {
|
|
137
149
|
const entries = [
|
|
138
150
|
metrics.sensitivity,
|
|
139
151
|
metrics.specificity,
|
|
140
152
|
metrics.ppv,
|
|
141
153
|
metrics.npv,
|
|
154
|
+
metrics.dor,
|
|
155
|
+
metrics.numberNeededToScreen,
|
|
142
156
|
metrics.lrPositive,
|
|
143
157
|
metrics.lrNegative,
|
|
144
158
|
metrics.preTestProbability,
|
|
@@ -152,6 +166,13 @@ function printReport(dataset, metrics) {
|
|
|
152
166
|
lines.push(dataset.name);
|
|
153
167
|
}
|
|
154
168
|
lines.push("");
|
|
169
|
+
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
170
|
+
lines.push("Heads up:");
|
|
171
|
+
for (const w of warnings) {
|
|
172
|
+
lines.push(` - ${w}`);
|
|
173
|
+
}
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
155
176
|
entries.forEach((metric) => {
|
|
156
177
|
lines.push(renderMetricLine(metric));
|
|
157
178
|
});
|
|
@@ -169,6 +190,11 @@ function printReport(dataset, metrics) {
|
|
|
169
190
|
|
|
170
191
|
function main() {
|
|
171
192
|
const args = parseArgs(process.argv.slice(2));
|
|
193
|
+
if (args.error) {
|
|
194
|
+
process.stderr.write(`${args.error}\n`);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
172
198
|
|
|
173
199
|
if (args.help) {
|
|
174
200
|
printHelp();
|
|
@@ -205,10 +231,34 @@ function main() {
|
|
|
205
231
|
return;
|
|
206
232
|
}
|
|
207
233
|
|
|
208
|
-
const
|
|
234
|
+
const continuity = typeof args.continuity === "string" ? args.continuity.toLowerCase() : "auto";
|
|
235
|
+
if (!["auto", "always", "never"].includes(continuity)) {
|
|
236
|
+
process.stderr.write(`Invalid --continuity value. Use auto, always, or never.\n`);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const metrics = core.calculateMetrics(built.input, { continuityCorrection: continuity });
|
|
241
|
+
if (args.format !== undefined && typeof args.format !== "string") {
|
|
242
|
+
process.stderr.write("Missing value for --format. Use --format text or --format json.\n");
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
209
246
|
const format = typeof args.format === "string" ? args.format.toLowerCase() : "text";
|
|
247
|
+
|
|
248
|
+
let chained = null;
|
|
249
|
+
if (args.chain) {
|
|
250
|
+
chained = buildChainedTest(args, metrics, continuity);
|
|
251
|
+
if (chained.error) {
|
|
252
|
+
process.stderr.write(`${chained.error}\n`);
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const warnings = [...core.buildBiasWarnings(built.input), ...datasetStore.buildDatasetWarnings(built.dataset)];
|
|
259
|
+
|
|
210
260
|
if (format === "json") {
|
|
211
|
-
printJsonReport(built.dataset, built.input, metrics);
|
|
261
|
+
printJsonReport(built.dataset, built.input, metrics, chained, warnings, continuity);
|
|
212
262
|
return;
|
|
213
263
|
}
|
|
214
264
|
|
|
@@ -218,7 +268,63 @@ function main() {
|
|
|
218
268
|
return;
|
|
219
269
|
}
|
|
220
270
|
|
|
221
|
-
|
|
271
|
+
const modified = built.dataset && ["tp", "fp", "fn", "tn", "preTestProb"].some((key) => built.input[key] !== built.dataset[key]);
|
|
272
|
+
printReport(built.dataset ? { ...built.dataset, name: `${modified ? "Customised: " : ""}${built.dataset.name}` } : null, metrics, warnings);
|
|
273
|
+
process.stdout.write(`Engine ${pkg.version}; continuity correction: ${continuity}.\n`);
|
|
274
|
+
if (chained) {
|
|
275
|
+
printChainedReport(chained);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildChainedTest(args, firstMetrics, continuity) {
|
|
280
|
+
const from = typeof args["chain-from"] === "string" ? args["chain-from"].toLowerCase() : "positive";
|
|
281
|
+
if (from !== "positive" && from !== "negative") {
|
|
282
|
+
return { error: "Invalid --chain-from. Use 'positive' or 'negative'." };
|
|
283
|
+
}
|
|
284
|
+
const sourceProb = from === "negative" ? firstMetrics.postTestNegative.value : firstMetrics.postTestPositive.value;
|
|
285
|
+
if (!Number.isFinite(sourceProb)) {
|
|
286
|
+
return { error: "Test 1's post-test probability is not finite; cannot chain." };
|
|
287
|
+
}
|
|
288
|
+
const preTestProb = core.chainedPreTestProbability(sourceProb);
|
|
289
|
+
|
|
290
|
+
const input = {
|
|
291
|
+
tp: typeof args.tp2 === "string" ? core.safeParseInt(args.tp2) : NaN,
|
|
292
|
+
fp: typeof args.fp2 === "string" ? core.safeParseInt(args.fp2) : NaN,
|
|
293
|
+
fn: typeof args.fn2 === "string" ? core.safeParseInt(args.fn2) : NaN,
|
|
294
|
+
tn: typeof args.tn2 === "string" ? core.safeParseInt(args.tn2) : NaN,
|
|
295
|
+
preTestProb,
|
|
296
|
+
};
|
|
297
|
+
const validation = core.validateInputs(input);
|
|
298
|
+
if (!validation.valid) {
|
|
299
|
+
return { error: `Test 2 validation failed: ${validation.message}` };
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
from,
|
|
303
|
+
input,
|
|
304
|
+
metrics: core.calculateMetrics(input, { continuityCorrection: continuity }),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function printChainedReport(chained) {
|
|
309
|
+
process.stdout.write(`\n— Chained second test (following test 1's ${chained.from} result) —\n`);
|
|
310
|
+
process.stdout.write("Assumption: results are conditionally independent given disease status.\n");
|
|
311
|
+
process.stdout.write(`Test 2 pre-test probability: ${core.formatPercentage(chained.input.preTestProb / 100)}\n\n`);
|
|
312
|
+
const entries = [
|
|
313
|
+
chained.metrics.sensitivity,
|
|
314
|
+
chained.metrics.specificity,
|
|
315
|
+
chained.metrics.ppv,
|
|
316
|
+
chained.metrics.npv,
|
|
317
|
+
chained.metrics.dor,
|
|
318
|
+
chained.metrics.numberNeededToScreen,
|
|
319
|
+
chained.metrics.lrPositive,
|
|
320
|
+
chained.metrics.lrNegative,
|
|
321
|
+
chained.metrics.postTestPositive,
|
|
322
|
+
chained.metrics.postTestNegative,
|
|
323
|
+
];
|
|
324
|
+
entries.forEach((metric) => {
|
|
325
|
+
process.stdout.write(`${renderMetricLine(metric)}\n`);
|
|
326
|
+
});
|
|
327
|
+
process.stdout.write("\n");
|
|
222
328
|
}
|
|
223
329
|
|
|
224
330
|
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Dataset provenance
|
|
2
|
+
|
|
3
|
+
All existing presets are classified `illustrative`. The repository does not retain a source table or extraction record establishing that the TP/FP/FN/TN values came from the cited medical papers. Keeping a DOI or a reference review date does not establish numerical provenance. The reference dates are therefore named `referenceLastReviewed`.
|
|
4
|
+
|
|
5
|
+
Each frozen preset has a `provenance` record: `kind`, `sourceLocation`, `extraction`, `threshold`, `referenceStandard`, `outcome`, `population`, `reviewStatus`, and `note`. Null fields mean evidence has not been retained. Browser warnings, terminal warnings and JSON snapshots carry this distinction; customised cases retain their original source identity and mark their counts modified.
|
|
6
|
+
|
|
7
|
+
To add a verified empirical case, retain an extraction file in this directory recording:
|
|
8
|
+
|
|
9
|
+
1. The exact paper version/DOI and table, page or supplementary location.
|
|
10
|
+
2. Population, exclusions, sample size, disease outcome and reference standard.
|
|
11
|
+
3. Test assay, units, cutoff and positive direction.
|
|
12
|
+
4. The four counts, their derivation and reconciliation against the source totals. Separate thresholds/cohorts must never be merged into a single ROC series.
|
|
13
|
+
5. Reviewer, review date and an independent check of all four cells.
|
|
14
|
+
|
|
15
|
+
Link that extraction from `provenance.extraction`, fill the remaining fields, add a regression fixture and update warnings only when the numerical claim is substantiated. No such independent extraction review was performed for the version 5 changes.
|
|
16
|
+
|
|
17
|
+
Simulated ROC points carry `simulated` provenance; editing them changes it to `edited-simulated`, preserving the qualification on AUC. A reference citation cannot convert a simulated point into an observed threshold.
|