diagcalc 3.2.4 → 5.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 +45 -8
- package/bin/diagcalc.js +142 -41
- package/docs/PROVENANCE.md +17 -0
- package/index.html +430 -118
- package/lib/README.md +98 -0
- package/lib/diagcalc-case.js +148 -0
- package/lib/diagcalc-core.js +554 -89
- package/lib/diagcalc-datasets.js +140 -1
- package/lib/diagcalc-geometry.js +27 -0
- package/lib/diagcalc-i18n.js +584 -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 +1269 -277
- package/styles.css +2231 -325
- package/tui/index.js +75 -272
- 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 +235 -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,14 +47,14 @@ 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
55
|
## Case Studies
|
|
56
56
|
|
|
57
|
-
|
|
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
58
|
|
|
59
59
|
- `screening` - low-prevalence population screening workflow
|
|
60
60
|
- `caseControl` - balanced case-control teaching example
|
|
@@ -71,7 +71,7 @@ These cases are included to support teaching across different prevalence setting
|
|
|
71
71
|
|
|
72
72
|
## Web App
|
|
73
73
|
|
|
74
|
-
The web app is static. It does not need a build step.
|
|
74
|
+
The web app is static. It does not need a build step. Its interface is available in English and European Portuguese, including scenarios, validation, history and chart labels. System fonts avoid external font downloads; neutral light/dark themes and responsive matrix tracks keep the calculator readable on phones, tablets and desktop screens.
|
|
75
75
|
|
|
76
76
|
It is intended for teaching sessions, demonstrations, and direct interactive exploration.
|
|
77
77
|
|
|
@@ -99,7 +99,7 @@ http://localhost:8080
|
|
|
99
99
|
|
|
100
100
|
## Terminal App
|
|
101
101
|
|
|
102
|
-
The terminal app requires Node.js
|
|
102
|
+
The terminal app requires Node.js 22 or newer.
|
|
103
103
|
|
|
104
104
|
It is intended for keyboard-first use, quick calculations, reproducible terminal workflows, and scripting.
|
|
105
105
|
|
|
@@ -165,6 +165,38 @@ The CLI is useful when you want a one-shot calculation or when you want to integ
|
|
|
165
165
|
- `r`: reset current case
|
|
166
166
|
- `q`: quit
|
|
167
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
|
+
|
|
168
200
|
## Deployment
|
|
169
201
|
|
|
170
202
|
### GitHub Pages
|
|
@@ -204,8 +236,13 @@ Package page:
|
|
|
204
236
|
|
|
205
237
|
- `index.html` - web app markup
|
|
206
238
|
- `styles.css` - web app styles
|
|
207
|
-
- `script.js` - web
|
|
208
|
-
- `
|
|
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
|
|
209
246
|
- `lib/diagcalc-datasets.js` - shared preset datasets
|
|
210
247
|
- `tui/index.js` - terminal UI
|
|
211
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,15 +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 });
|
|
209
241
|
if (args.format !== undefined && typeof args.format !== "string") {
|
|
210
242
|
process.stderr.write("Missing value for --format. Use --format text or --format json.\n");
|
|
211
243
|
process.exitCode = 1;
|
|
212
244
|
return;
|
|
213
245
|
}
|
|
214
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
|
+
|
|
215
260
|
if (format === "json") {
|
|
216
|
-
printJsonReport(built.dataset, built.input, metrics);
|
|
261
|
+
printJsonReport(built.dataset, built.input, metrics, chained, warnings, continuity);
|
|
217
262
|
return;
|
|
218
263
|
}
|
|
219
264
|
|
|
@@ -223,7 +268,63 @@ function main() {
|
|
|
223
268
|
return;
|
|
224
269
|
}
|
|
225
270
|
|
|
226
|
-
|
|
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");
|
|
227
328
|
}
|
|
228
329
|
|
|
229
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.
|