@pablotech/akesi 0.1.32 → 0.1.34
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/dates.ts +1 -1
- package/finding-assemble.ts +2 -2
- package/marker-deltas.ts +4 -3
- package/package.json +1 -1
- package/pinned-queries.ts +1 -1
- package/report-merge.ts +1 -1
- package/treatment-bucket.ts +13 -13
- package/treatment-infer.ts +1 -1
- package/types.ts +5 -5
- package/unit-systems.ts +6 -6
package/dates.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function formatDay(iso: string | undefined | null): string {
|
|
|
27
27
|
return `${MONTHS[Number(m) - 1]} ${Number(d)}, ${y}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// A type="date" input fires `change` on every keystroke inside an already-plausible year
|
|
31
31
|
// segment, zero-padded (e.g. typing just "2" of "2026" reports "0002-08-15") — not only once the
|
|
32
32
|
// full year is typed. A plain truthiness/length check on `value` fires on that first padded digit.
|
|
33
33
|
// Require a 4-digit year that isn't itself a padding artifact (<1000) before treating it as done.
|
package/finding-assemble.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface FindingAIResponse {
|
|
|
22
22
|
};
|
|
23
23
|
/** Leaf-owned: absent from a core response, present on an older stored finding. */
|
|
24
24
|
studyResults?: { study: string; result: string; group: string }[];
|
|
25
|
-
//
|
|
25
|
+
// No `noteId`/label field here: a note has no short verbatim-echoable label like Study's
|
|
26
26
|
// `focus`, so entries are paired back to `factors.noteEntries` by array position, not content.
|
|
27
27
|
// See assembleFinding's `meta.noteIds` (the caller supplies the parallel id array).
|
|
28
28
|
/** Leaf-owned: absent from a core response, present on an older stored finding. */
|
|
@@ -673,7 +673,7 @@ export function assembleFinding(
|
|
|
673
673
|
inputsHash: string;
|
|
674
674
|
nodeHashes: Record<string, string>;
|
|
675
675
|
generatedBy: ClientFinding["generatedBy"];
|
|
676
|
-
//
|
|
676
|
+
// The ordered ids of the Note entries the prompt presented (populatedNoteEntries), zipped
|
|
677
677
|
// positionally against parsed.noteResults since a note has no label the LLM could echo back.
|
|
678
678
|
noteIds: string[];
|
|
679
679
|
// 06/Gap A — injected rather than looked up here, so this module carries no dependency on
|
package/marker-deltas.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Cross-study deltas
|
|
1
|
+
// Cross-study deltas. The single source of truth for "what changed" — shared
|
|
2
2
|
// by a host's chart/print surfaces and by the Finding prompt builder here, so the change
|
|
3
3
|
// shown on screen and the change the model reasons over can never disagree.
|
|
4
4
|
//
|
|
5
|
-
// Grouping is by stored marker name, which is canonical
|
|
6
|
-
// so a metric is one series. `fromComparison` rows
|
|
5
|
+
// Grouping is by stored marker name, which is canonical once imaging-catalog markers are
|
|
6
|
+
// stored the same way as lab markers, so a metric is one series. `fromComparison` rows are
|
|
7
|
+
// real datapoints here; a
|
|
7
8
|
// directly-measured reading at the same marker|date already won at ingest.
|
|
8
9
|
|
|
9
10
|
import type { Client, MarkerResult } from "./types";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pablotech/akesi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "Schema-constrained clinical reasoning over lab and marker data, where every constraint stated to the model in prose is re-enforced in code on the response.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/pinned-queries.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function pinnedQueries(client: Client): PinnedQuery[] {
|
|
|
61
61
|
if (t) out.push({ section, label: clip(t) });
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// On the owner's instruction (2026-08-20): a ratio name used to be barred from the
|
|
65
65
|
// prompt outright. As an area of query it is safe and useful — "look at the TG/HDL ratio" is a
|
|
66
66
|
// topic, not a reading — and the block below is explicit that it is not data.
|
|
67
67
|
for (const name of client.pinnedRatios ?? []) push("Marker ratios", name);
|
package/report-merge.ts
CHANGED
|
@@ -297,7 +297,7 @@ export interface SourceEditPatch {
|
|
|
297
297
|
date: string;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
//
|
|
300
|
+
// Patch ONE source and its linked DiseaseEntry rows (by position, in sourceId order) in
|
|
301
301
|
// one mutation, keyed by sourceId — the patch counterpart to removeSource's drop. `diseasePatches`
|
|
302
302
|
// must line up 1:1 with diseasesForSource(sourceId)'s current order; a short/mismatched array is
|
|
303
303
|
// ignored past its own length (no add/remove of diagnoses here). Raw values in — normalizeClientDraft
|
package/treatment-bucket.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { formatDay } from "./dates";
|
|
|
12
12
|
|
|
13
13
|
export type Bucket = "past" | "ongoing" | "planned";
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// The display label for a bucket badge (Treatment's Ungrouped view, which
|
|
16
16
|
// otherwise concatenates all three buckets with no other visual distinction).
|
|
17
17
|
export const BUCKET_LABEL: Record<Bucket, string> = { ongoing: "Ongoing", planned: "Planned", past: "Past" };
|
|
18
18
|
|
|
@@ -73,7 +73,7 @@ export function collapseByName(items: TreatmentItem[]): TreatmentItem[] {
|
|
|
73
73
|
id: latest.id,
|
|
74
74
|
name: latest.name,
|
|
75
75
|
dose: latest.dose,
|
|
76
|
-
//
|
|
76
|
+
// Carry the structured dose fields through too, same "latest row wins" rule as `dose`
|
|
77
77
|
// itself; formatDose() prefers these over `dose`, so dropping them here would silently blank
|
|
78
78
|
// out any collapsed-view display for a titration step entered through the new Amount/Unit form.
|
|
79
79
|
doseAmount: latest.doseAmount,
|
|
@@ -110,16 +110,16 @@ export interface NamedTreatmentGroup {
|
|
|
110
110
|
rows: TreatmentItem[];
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
//
|
|
113
|
+
// Primary group order: Planned drugs first, then Ongoing, then Past — recency of use. A
|
|
114
114
|
// group's bucket is its NEWEST row's (rows[0], already sorted descending below), so a drug
|
|
115
115
|
// currently mid-titration reads as Ongoing even if an older row of the same drug once looked
|
|
116
|
-
// Planned.
|
|
116
|
+
// Planned. Within one bucket, groups sort alphabetically (secondary key), not entry order.
|
|
117
117
|
const GROUP_BUCKET_ORDER: Record<Bucket, number> = { planned: 0, ongoing: 1, past: 2 };
|
|
118
118
|
|
|
119
|
-
//
|
|
119
|
+
// The Medicine audit view's grouping: every raw row for a drug together, unlike
|
|
120
120
|
// collapseByName which discards all but one representative row per name.
|
|
121
121
|
//
|
|
122
|
-
//
|
|
122
|
+
// Rows sorted newest-first (start descending): the table must always read newest-to-oldest,
|
|
123
123
|
// top to bottom. dateGaps() below is written against this same descending order.
|
|
124
124
|
export function groupByName(items: TreatmentItem[], today: string): NamedTreatmentGroup[] {
|
|
125
125
|
const byName = new Map<string, TreatmentItem[]>();
|
|
@@ -140,31 +140,31 @@ export function groupByName(items: TreatmentItem[], today: string): NamedTreatme
|
|
|
140
140
|
|
|
141
141
|
export type DateGapVerdict = "ok" | "gap" | "overlap";
|
|
142
142
|
|
|
143
|
-
//
|
|
143
|
+
// Whole-day distance between two full YYYY-MM-DD dates (nextStart - end), or null if
|
|
144
144
|
// either isn't full day precision (legacy month/year-only can't do calendar-day arithmetic).
|
|
145
145
|
function daysBetween(a: string, b: string): number | null {
|
|
146
146
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(a) || !/^\d{4}-\d{2}-\d{2}$/.test(b)) return null;
|
|
147
147
|
return Math.round((Date.parse(b) - Date.parse(a)) / 86400000);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
//
|
|
150
|
+
// One verdict per consecutive pair in an already-sorted `rows` (see groupByName), comparing
|
|
151
151
|
// the OLDER row's end against the NEWER row's start: no end (still ongoing) or an unknown start
|
|
152
152
|
// can't be judged, so those read "ok" rather than a false positive. Length is rows.length - 1.
|
|
153
153
|
//
|
|
154
|
-
//
|
|
154
|
+
// A titration step ending 2026-06-30 with the next starting 2026-07-01 is back-to-back
|
|
155
155
|
// coverage, not a gap: prefer day-precision arithmetic (0 or 1 day apart both read "ok") over the
|
|
156
156
|
// cmp() prefix comparison, which only recognized an exact same-day handoff as contiguous. Falls
|
|
157
157
|
// back to cmp() when either date isn't full day precision.
|
|
158
158
|
//
|
|
159
|
-
//
|
|
159
|
+
// `rows` is newest-first (groupByName sorts descending), so for the pair at (i, i+1), i+1
|
|
160
160
|
// is the OLDER row and i is the NEWER one — verdict[i] describes the gap/overlap between them.
|
|
161
161
|
//
|
|
162
|
-
//
|
|
162
|
+
// Symmetric with the gap side: a 1-day overlap (the newer row starts the day before the
|
|
163
163
|
// older one's recorded end — a rounding/entry-day wobble, not a real double-dosing period) also
|
|
164
164
|
// reads "ok". Only a 2+ day overlap is worth flagging. This only applies to the day-precision
|
|
165
165
|
// branch — the cmp() fallback (month/year-only dates) has no day-scale magnitude to be lenient about.
|
|
166
166
|
//
|
|
167
|
-
//
|
|
167
|
+
// An AM row and a PM row covering the same dates aren't double-dosing, they're a twice-daily
|
|
168
168
|
// split — only downgrades a would-be "overlap" (a real gap between an AM and a PM step is still a
|
|
169
169
|
// gap; this isn't a blanket "ignore timing" exemption).
|
|
170
170
|
function splitByTiming(a: TreatmentItem, b: TreatmentItem): boolean {
|
|
@@ -198,7 +198,7 @@ export function dateGaps(rows: TreatmentItem[]): DateGapVerdict[] {
|
|
|
198
198
|
// The verbatim label a PLANNED treatment presents to the Finding (Patient Plan Action, treatmentGroups
|
|
199
199
|
// patient ref, planAssessmentRows key) — its name plus dose when present. Must be identical everywhere
|
|
200
200
|
// the ref is matched (finding-generate input block, finding-assemble expected set, treatment-groups resolve).
|
|
201
|
-
//
|
|
201
|
+
// The display string for a dose: structured amount/unit/frequency when the record has been
|
|
202
202
|
// entered/edited through the new form, reproducing the pre-existing "6mg/week" convention so
|
|
203
203
|
// treatmentLabel()'s output (and everything matched against it) doesn't change shape; falls back to
|
|
204
204
|
// the legacy free-text `dose` string for anything a host has not yet migrated.
|
package/treatment-infer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Treatment add-flow intake: read what the user actually has about a product — photos of a
|
|
2
2
|
// bottle/package/label, or the product sheet as pasted text — and propose a record. Pure of
|
|
3
3
|
// Node/process/env, mirroring report-extract.ts's shape. The Anthropic client is INJECTED and the
|
|
4
4
|
// model is a required arg (no default), so this module never touches process.env or the config.
|
package/types.ts
CHANGED
|
@@ -128,7 +128,7 @@ export interface TreatmentItem {
|
|
|
128
128
|
id: string;
|
|
129
129
|
pinned?: boolean;
|
|
130
130
|
name: string;
|
|
131
|
-
//
|
|
131
|
+
// Superseded by doseAmount/doseUnit/doseFrequency (below) for anything entered through the
|
|
132
132
|
// edit form; kept as the display/matching fallback for un-migrated records (see formatDose() in
|
|
133
133
|
// treatment-bucket.ts, and whatever one-time conversion a host runs).
|
|
134
134
|
dose?: string;
|
|
@@ -429,15 +429,15 @@ export interface ClientFinding {
|
|
|
429
429
|
// findings without it still render (StudyResults falls back to a flat list).
|
|
430
430
|
studyResults?: { study: string; result: string; group?: string }[];
|
|
431
431
|
// One inference per populated Note row (factors.noteEntries); keyed by `noteId` rather than a
|
|
432
|
-
// label
|
|
433
|
-
// free prose, unreliable for the LLM to echo back verbatim for pairing
|
|
432
|
+
// label — unlike Study's short hand-picked `focus` labels, a note's `text` is unbounded
|
|
433
|
+
// free prose, unreliable for the LLM to echo back verbatim for pairing.
|
|
434
434
|
noteResults?: { noteId: string; result: string; group?: string }[];
|
|
435
|
-
//
|
|
435
|
+
// One inference per known allergy (factors.allergies) / family history entry
|
|
436
436
|
// (factors.familyHistory), keyed by id like noteResults, paired by array position (no natural
|
|
437
437
|
// unique label to echo back).
|
|
438
438
|
allergyResults?: { allergyId: string; result: string; group?: string }[];
|
|
439
439
|
familyResults?: { familyId: string; result: string; group?: string }[];
|
|
440
|
-
//
|
|
440
|
+
// Mirrors allergyResults/familyResults exactly, for Reports diagnoses (factors.diseases),
|
|
441
441
|
// which previously had no AI-paired leaf at all.
|
|
442
442
|
diseaseResults?: { diseaseId: string; result: string; group?: string }[];
|
|
443
443
|
disease: { group: string; finding: string }[];
|
package/unit-systems.ts
CHANGED
|
@@ -36,7 +36,7 @@ const IMPERIAL_TO_METRIC: Record<string, { unit: string; factor: number }> = {
|
|
|
36
36
|
"in³": { unit: "cm³", factor: 16.387064 },
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// Markers where the metric unit IS the US-prevailing clinical convention, so the
|
|
40
40
|
// generic PHYSICAL_TO_IMPERIAL unit-keyed table must NOT convert them (e.g. visceral adipose
|
|
41
41
|
// tissue mass is reported in grams in US practice too, never ounces/lb — the owner's example,
|
|
42
42
|
// extended here to the sibling DEXA body-composition markers sharing the same convention).
|
|
@@ -49,7 +49,7 @@ export const PHYSICAL_NO_CONVERT = new Set<string>([
|
|
|
49
49
|
"Subcutaneous adipose tissue area",
|
|
50
50
|
]);
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// Cosmetic unit-STRING variants for the same physical unit, straight from different lab
|
|
53
53
|
// source formats (values stay as-reported per the module doc above; only the DISPLAYED label is
|
|
54
54
|
// canonicalized, never the stored data). eGFR renders identically regardless of which lab
|
|
55
55
|
// formatted its unit string.
|
|
@@ -61,7 +61,7 @@ export function canonicalUnit(unit: string): string {
|
|
|
61
61
|
return UNIT_ALIAS[unit] ?? unit;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// Compound clinical units that are, on research, reported identically in US and SI/
|
|
65
65
|
// international practice — no US-customary form exists in routine use, so no conversion ever
|
|
66
66
|
// applies regardless of marker. NEEDS VERIFICATION against a clinical reference before treating
|
|
67
67
|
// as final; each entry's marker(s) are noted so a reviewer can check the specific convention.
|
|
@@ -84,7 +84,7 @@ export interface AnalyteRule {
|
|
|
84
84
|
}
|
|
85
85
|
export const ANALYTE: Record<string, AnalyteRule> = {
|
|
86
86
|
// Lipid/glucose panel — mirrors the in-repo ingest table (parsers/healthmatters.ts
|
|
87
|
-
// NORMALIZE
|
|
87
|
+
// NORMALIZE); 1/k equals that table's factor (cross-checked by test).
|
|
88
88
|
Glucose: { us: "mg/dL", si: "mmol/L", k: 1 / 18.02 }, // glucose MW 180.16
|
|
89
89
|
"Estimated Average Glucose (eAG)": { us: "mg/dL", si: "mmol/L", k: 1 / 18.02 },
|
|
90
90
|
"Total Cholesterol": { us: "mg/dL", si: "mmol/L", k: 1 / 38.67 }, // chol MW 386.65
|
|
@@ -133,7 +133,7 @@ export const ANALYTE: Record<string, AnalyteRule> = {
|
|
|
133
133
|
// Monovalent electrolytes: mEq/L (US) and mmol/L (SI) are numerically identical (valence 1).
|
|
134
134
|
"Potassium, Serum (Kalium)": { us: "mEq/L", si: "mmol/L", k: 1 },
|
|
135
135
|
"Sodium, Serum (Natrium)": { us: "mEq/L", si: "mmol/L", k: 1 },
|
|
136
|
-
//
|
|
136
|
+
// Urine albumin/creatinine ratio: US commonly reports mg/g creatinine; UK/Canada/
|
|
137
137
|
// Australia and KDIGO's international staging table use mg/mmol creatinine. Factor derived
|
|
138
138
|
// from creatinine MW 113.12 g/mol (1 g creatinine = 8.84 mmol, so mg/mmol = mg/g × 0.1131).
|
|
139
139
|
// NEEDS VERIFICATION against a clinical reference before relying on this — flagged per the
|
|
@@ -144,7 +144,7 @@ export const ANALYTE: Record<string, AnalyteRule> = {
|
|
|
144
144
|
|
|
145
145
|
// The units that genuinely differ US↔SI, or need an explicit verified-non-convertible decision —
|
|
146
146
|
// used by the coverage gate to flag any (marker, unit) pair carrying one of these WITHOUT a rule
|
|
147
|
-
// (so a gap can never be silent)
|
|
147
|
+
// (so a gap can never be silent) — extended beyond molar-concentration units to the
|
|
148
148
|
// compound/physical units researched above.
|
|
149
149
|
export const CONVERTIBLE_UNIT_CLASS = new Set([
|
|
150
150
|
"mg/dL", "ng/dL", "pg/mL", "µg/dL", "ug/dL", "mcg/dL", "ng/mL",
|