@peopl-health/nexus 5.1.0-dev.467 → 6.0.0-dev.466
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/lib/clinical/models/turnTraceModel.js +0 -11
- package/lib/clinical/services/fhirService.js +14 -12
- package/lib/utils/jsonUtils.js +1 -34
- package/package.json +1 -1
- package/lib/clinical/config/divergenceConfig.js +0 -48
- package/lib/clinical/helpers/divergenceHelper.js +0 -43
- package/lib/clinical/helpers/projectorHelper.js +0 -18
- package/lib/clinical/helpers/seedHelper.js +0 -62
- package/lib/clinical/services/seedService.js +0 -21
|
@@ -44,17 +44,6 @@ turnTraceSchema.methods.addStep = function addStep(step) { this.steps.push({ ...
|
|
|
44
44
|
turnTraceSchema.methods.setStatus = function setStatus(status, abort = null) { this.status = status; this.abort = abort; };
|
|
45
45
|
turnTraceSchema.methods.setTimings = function setTimings(timings) { this.timings = { ...this.timings, ...timings }; };
|
|
46
46
|
|
|
47
|
-
turnTraceSchema.methods.toDecision = function toDecision() {
|
|
48
|
-
const decision = this.safetyDecision || {};
|
|
49
|
-
const signals = this.signals || {};
|
|
50
|
-
return {
|
|
51
|
-
escalation_kind: decision.escalationKind || null,
|
|
52
|
-
outbound_purpose: signals.outboundPurpose || null,
|
|
53
|
-
branch_taken: signals.branchTaken || null,
|
|
54
|
-
red_flags_matched: decision.redFlagsMatched || [],
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
|
|
58
47
|
turnTraceSchema.methods.setPrompt = function setPrompt({ resolved, resolvedPresetId, resolvedPresetVersion }) {
|
|
59
48
|
this.prompt = {
|
|
60
49
|
resolved: resolved || null,
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
const { load } = require('../config/fhirConfig');
|
|
2
|
-
const { createProjectorRegistry } = require('../helpers/projectorHelper');
|
|
3
2
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
.map((resource) => ({ resource }));
|
|
10
|
-
return { resourceType: 'Bundle', type: 'collection', entry };
|
|
11
|
-
},
|
|
12
|
-
});
|
|
3
|
+
const projectors = new Map();
|
|
4
|
+
|
|
5
|
+
function registerProjector(name, fn) {
|
|
6
|
+
projectors.set(name, fn);
|
|
7
|
+
}
|
|
13
8
|
|
|
14
9
|
async function project(items) {
|
|
15
10
|
await load();
|
|
16
|
-
|
|
11
|
+
const entry = items
|
|
12
|
+
.flatMap(({ name, aggregate }) => {
|
|
13
|
+
const fn = projectors.get(name);
|
|
14
|
+
if (!fn) throw new Error(`no FHIR projector registered for '${name}'`);
|
|
15
|
+
return (fn(aggregate) || []).flat();
|
|
16
|
+
})
|
|
17
|
+
.map((resource) => ({ resource }));
|
|
18
|
+
return { resourceType: 'Bundle', type: 'collection', entry };
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
module.exports = {
|
|
20
|
-
registerProjector
|
|
22
|
+
registerProjector,
|
|
21
23
|
project,
|
|
22
24
|
};
|
package/lib/utils/jsonUtils.js
CHANGED
|
@@ -9,12 +9,6 @@ function jsonStringifyWithUnicodeEscapes(obj) {
|
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function isPlainObject(value) {
|
|
13
|
-
if (value === null || typeof value !== 'object') return false;
|
|
14
|
-
const proto = Object.getPrototypeOf(value);
|
|
15
|
-
return proto === Object.prototype || proto === null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
12
|
function safeParse(value, fallback = {}) {
|
|
19
13
|
try {
|
|
20
14
|
return JSON.parse(value || JSON.stringify(fallback));
|
|
@@ -23,31 +17,4 @@ function safeParse(value, fallback = {}) {
|
|
|
23
17
|
}
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
const parsed = safeParse(value, fallback);
|
|
28
|
-
return Array.isArray(parsed) ? parsed : fallback;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function cleanForJson(value) {
|
|
32
|
-
if (value === null || value === undefined) return value;
|
|
33
|
-
if (value instanceof Date) return value.toISOString();
|
|
34
|
-
if (typeof value === 'object' && typeof value.toHexString === 'function') return value.toHexString();
|
|
35
|
-
if (Array.isArray(value)) return value.map(cleanForJson);
|
|
36
|
-
if (typeof value === 'object') {
|
|
37
|
-
const out = {};
|
|
38
|
-
for (const [key, val] of Object.entries(value)) {
|
|
39
|
-
if (key === '_id' || key === '__v') continue;
|
|
40
|
-
out[key] = cleanForJson(val);
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
module.exports = {
|
|
48
|
-
jsonStringifyWithUnicodeEscapes,
|
|
49
|
-
safeParse,
|
|
50
|
-
safeParseArray,
|
|
51
|
-
cleanForJson,
|
|
52
|
-
isPlainObject,
|
|
53
|
-
};
|
|
20
|
+
module.exports = { jsonStringifyWithUnicodeEscapes, safeParse };
|
package/package.json
CHANGED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
const { Config_ID } = require('../../config/airtableConfig');
|
|
2
|
-
|
|
3
|
-
const { getRecordByFilter } = require('../../services/airtableService');
|
|
4
|
-
|
|
5
|
-
const MapCache = require('../../utils/MapCache');
|
|
6
|
-
const { safeParseArray } = require('../../utils/jsonUtils');
|
|
7
|
-
|
|
8
|
-
const DIVERGENCE_CONFIG_TABLE = 'divergence';
|
|
9
|
-
const CACHE_TTL = 5 * 60 * 1000;
|
|
10
|
-
const CACHE_KEY = 'divergenceConfig';
|
|
11
|
-
const COMPARATOR_TYPES = ['scalar', 'set'];
|
|
12
|
-
|
|
13
|
-
const cache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
14
|
-
|
|
15
|
-
async function load() {
|
|
16
|
-
const cached = cache.get(CACHE_KEY);
|
|
17
|
-
if (cached) return cached;
|
|
18
|
-
|
|
19
|
-
const records = await getRecordByFilter(Config_ID, DIVERGENCE_CONFIG_TABLE, 'TRUE()');
|
|
20
|
-
if (!records) throw new Error('divergence config could not be read from the Config base');
|
|
21
|
-
const byKey = Object.fromEntries(records.map((r) => [r.config, r.value]));
|
|
22
|
-
|
|
23
|
-
if (!byKey.FIELDS) throw new Error('divergence config missing from Config base: FIELDS');
|
|
24
|
-
const fields = safeParseArray(byKey.FIELDS);
|
|
25
|
-
if (!fields) throw new Error('divergence config FIELDS is not a JSON array');
|
|
26
|
-
if (!fields.length) throw new Error('divergence config FIELDS is empty; nothing would be compared');
|
|
27
|
-
|
|
28
|
-
const unknown = fields.filter(({ type }) => !COMPARATOR_TYPES.includes(type));
|
|
29
|
-
if (unknown.length) throw new Error(`divergence config FIELDS has unknown types: ${unknown.map(({ field, type }) => `${field}:${type}`).join(', ')}`);
|
|
30
|
-
|
|
31
|
-
const misplaced = fields.filter(({ type, emergencyKinds }) => emergencyKinds && type !== 'scalar');
|
|
32
|
-
if (misplaced.length) throw new Error(`divergence config emergencyKinds only applies to scalar fields: ${misplaced.map(({ field }) => field).join(', ')}`);
|
|
33
|
-
|
|
34
|
-
const config = { FIELDS: fields };
|
|
35
|
-
cache.set(CACHE_KEY, config);
|
|
36
|
-
return config;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function current() {
|
|
40
|
-
const cached = cache.get(CACHE_KEY);
|
|
41
|
-
if (!cached) throw new Error('divergence config not loaded; call load() first');
|
|
42
|
-
return cached;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
module.exports = {
|
|
46
|
-
load,
|
|
47
|
-
getFields: () => current().FIELDS,
|
|
48
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
const { isDeepStrictEqual } = require('node:util');
|
|
2
|
-
|
|
3
|
-
const COMPARATORS = {
|
|
4
|
-
scalar: (expected, actual) => expected === actual,
|
|
5
|
-
set: (expected, actual) => isDeepStrictEqual(new Set(expected || []), new Set(actual || [])),
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const OUTCOME_RULES = [
|
|
9
|
-
[({ deterministic }) => deterministic.divergences.some((d) => d.isEmergencyFlip), 'review'],
|
|
10
|
-
[({ stochastic }) => stochastic.verdict === 'fail', 'fail'],
|
|
11
|
-
[({ deterministic }) => !deterministic.hasPassed, 'diverged'],
|
|
12
|
-
[({ stochastic }) => stochastic.verdict === 'review', 'review'],
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
function deterministicDivergence(expected = {}, actual = {}, fields) {
|
|
16
|
-
const divergences = fields
|
|
17
|
-
.filter(({ field, type }) => {
|
|
18
|
-
const compare = COMPARATORS[type];
|
|
19
|
-
if (!compare) throw new Error(`unknown divergence field type '${type}'`);
|
|
20
|
-
return !compare(expected?.[field], actual?.[field]);
|
|
21
|
-
})
|
|
22
|
-
.map(({ field, emergencyKinds = [] }) => ({
|
|
23
|
-
field,
|
|
24
|
-
expected: expected?.[field],
|
|
25
|
-
actual: actual?.[field],
|
|
26
|
-
isEmergencyFlip: emergencyKinds.includes(expected?.[field]) || emergencyKinds.includes(actual?.[field]),
|
|
27
|
-
}));
|
|
28
|
-
return { divergences, hasPassed: divergences.length === 0 };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function classify(result) {
|
|
32
|
-
return OUTCOME_RULES.find(([applies]) => applies(result))?.[1] || 'pass';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stochasticDivergence() {
|
|
36
|
-
return { verdict: 'review', rationale: 'stochastic message grading not yet enabled', isGraded: false };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = {
|
|
40
|
-
deterministicDivergence,
|
|
41
|
-
classify,
|
|
42
|
-
stochasticDivergence,
|
|
43
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
function createProjectorRegistry({ assemble, label = 'projector' }) {
|
|
2
|
-
const projectors = new Map();
|
|
3
|
-
return {
|
|
4
|
-
register: (name, fn) => projectors.set(name, fn),
|
|
5
|
-
project: (items) =>
|
|
6
|
-
assemble(
|
|
7
|
-
items.map(({ name, aggregate }) => {
|
|
8
|
-
const fn = projectors.get(name);
|
|
9
|
-
if (!fn) throw new Error(`no ${label} registered for '${name}'`);
|
|
10
|
-
return { name, content: fn(aggregate) };
|
|
11
|
-
}),
|
|
12
|
-
),
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
module.exports = {
|
|
17
|
-
createProjectorRegistry,
|
|
18
|
-
};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
const { cleanForJson, isPlainObject } = require('../../utils/jsonUtils');
|
|
2
|
-
|
|
3
|
-
const SEED_FORMATS = {
|
|
4
|
-
'.json': {
|
|
5
|
-
isValid: (c) => isPlainObject(c) && Object.values(c).every(isPlainObject),
|
|
6
|
-
hint: 'an object keyed by id whose values are plain objects',
|
|
7
|
-
},
|
|
8
|
-
'.jsonl': {
|
|
9
|
-
isValid: (c) => Array.isArray(c) && c.every(isPlainObject),
|
|
10
|
-
hint: 'a list of plain objects',
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// clinical-agent file-store shapes:
|
|
15
|
-
// keyedFile → KeyedJSONFile (`{pk: doc}` written as `.json`)
|
|
16
|
-
// jsonlFile → JSONLFileStore (list-of-dicts written as `.jsonl`)
|
|
17
|
-
function keyedFile(records, primaryKey) {
|
|
18
|
-
const out = {};
|
|
19
|
-
for (const record of records) {
|
|
20
|
-
const doc = cleanForJson(record);
|
|
21
|
-
const key = doc[primaryKey];
|
|
22
|
-
if (key === undefined || key === null || key === '') {
|
|
23
|
-
throw new Error(`keyed store record is missing primary key '${primaryKey}'`);
|
|
24
|
-
}
|
|
25
|
-
const skey = String(key);
|
|
26
|
-
if (Object.prototype.hasOwnProperty.call(out, skey)) {
|
|
27
|
-
throw new Error(`keyed store has duplicate primary key '${primaryKey}' value '${skey}'`);
|
|
28
|
-
}
|
|
29
|
-
out[skey] = doc;
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function jsonlFile(records) {
|
|
35
|
-
return records.map(cleanForJson);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function validateSeed(seed) {
|
|
39
|
-
const basenames = new Set();
|
|
40
|
-
for (const [name, content] of Object.entries(seed)) {
|
|
41
|
-
const basename = name.split('/').pop();
|
|
42
|
-
if (!basename || basename === '.' || basename === '..') {
|
|
43
|
-
throw new Error(`invalid seed file name: '${name}'`);
|
|
44
|
-
}
|
|
45
|
-
if (basenames.has(basename)) {
|
|
46
|
-
throw new Error(`duplicate seed file basename: '${basename}'`);
|
|
47
|
-
}
|
|
48
|
-
basenames.add(basename);
|
|
49
|
-
|
|
50
|
-
const ext = Object.keys(SEED_FORMATS).find((extension) => basename.endsWith(extension));
|
|
51
|
-
const format = ext ? SEED_FORMATS[ext] : null;
|
|
52
|
-
if (!format) throw new Error(`seed file must end in .json or .jsonl: '${name}'`);
|
|
53
|
-
if (!format.isValid(content)) throw new Error(`'${name}' must be ${format.hint}`);
|
|
54
|
-
}
|
|
55
|
-
return seed;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
module.exports = {
|
|
59
|
-
keyedFile,
|
|
60
|
-
jsonlFile,
|
|
61
|
-
validateSeed,
|
|
62
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const { createProjectorRegistry } = require('../helpers/projectorHelper');
|
|
2
|
-
const { validateSeed } = require('../helpers/seedHelper');
|
|
3
|
-
|
|
4
|
-
const registry = createProjectorRegistry({
|
|
5
|
-
label: 'seed projector',
|
|
6
|
-
assemble: (items) => {
|
|
7
|
-
const seed = {};
|
|
8
|
-
const seen = new Set();
|
|
9
|
-
for (const { name, content } of items) {
|
|
10
|
-
if (seen.has(name)) throw new Error(`duplicate seed file name: '${name}'`);
|
|
11
|
-
seen.add(name);
|
|
12
|
-
if (content !== undefined) seed[name] = content;
|
|
13
|
-
}
|
|
14
|
-
return validateSeed(seed);
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
module.exports = {
|
|
19
|
-
registerSeedProjector: registry.register,
|
|
20
|
-
projectSeed: registry.project,
|
|
21
|
-
};
|