@fnndsc/brasa 0.16.1 → 0.17.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.
Files changed (59) hide show
  1. package/dist/api/index.js +1 -0
  2. package/dist/api/index.js.map +1 -1
  3. package/dist/buildinfo.json +1 -1
  4. package/dist/builtins/fs/acl.js +57 -1
  5. package/dist/builtins/fs/acl.js.map +1 -1
  6. package/dist/builtins/fs/cat.js +46 -9
  7. package/dist/builtins/fs/cat.js.map +1 -1
  8. package/dist/builtins/fs/cp.js +42 -2
  9. package/dist/builtins/fs/cp.js.map +1 -1
  10. package/dist/builtins/fs/destination.d.ts +52 -0
  11. package/dist/builtins/fs/destination.js +52 -0
  12. package/dist/builtins/fs/destination.js.map +1 -0
  13. package/dist/builtins/fs/download.js +5 -2
  14. package/dist/builtins/fs/download.js.map +1 -1
  15. package/dist/builtins/fs/mv.js +41 -1
  16. package/dist/builtins/fs/mv.js.map +1 -1
  17. package/dist/builtins/fs/rm.d.ts +6 -0
  18. package/dist/builtins/fs/rm.js +35 -17
  19. package/dist/builtins/fs/rm.js.map +1 -1
  20. package/dist/builtins/fs/upload.js +15 -4
  21. package/dist/builtins/fs/upload.js.map +1 -1
  22. package/dist/builtins/help.js +43 -6
  23. package/dist/builtins/help.js.map +1 -1
  24. package/dist/builtins/net/pacs.js +37 -1
  25. package/dist/builtins/net/pacs.js.map +1 -1
  26. package/dist/builtins/net/query.csv.d.ts +73 -0
  27. package/dist/builtins/net/query.csv.js +267 -0
  28. package/dist/builtins/net/query.csv.js.map +1 -0
  29. package/dist/builtins/net/query.d.ts +28 -1
  30. package/dist/builtins/net/query.fanout.d.ts +92 -0
  31. package/dist/builtins/net/query.fanout.js +171 -0
  32. package/dist/builtins/net/query.fanout.js.map +1 -0
  33. package/dist/builtins/net/query.js +709 -20
  34. package/dist/builtins/net/query.js.map +1 -1
  35. package/dist/builtins/net/status.js +4 -1
  36. package/dist/builtins/net/status.js.map +1 -1
  37. package/dist/builtins/proc.js +18 -0
  38. package/dist/builtins/proc.js.map +1 -1
  39. package/dist/builtins/res/feed.js +43 -2
  40. package/dist/builtins/res/feed.js.map +1 -1
  41. package/dist/builtins/res/plugin.info.d.ts +32 -0
  42. package/dist/builtins/res/plugin.info.js +167 -0
  43. package/dist/builtins/res/plugin.info.js.map +1 -0
  44. package/dist/builtins/res/plugin.js +41 -1
  45. package/dist/builtins/res/plugin.js.map +1 -1
  46. package/dist/core/engine.d.ts +21 -0
  47. package/dist/core/engine.js +27 -0
  48. package/dist/core/engine.js.map +1 -1
  49. package/dist/core/promptContext.d.ts +1 -1
  50. package/dist/core/question.d.ts +27 -0
  51. package/dist/core/question.js +38 -0
  52. package/dist/core/question.js.map +1 -1
  53. package/dist/core/surface.d.ts +16 -3
  54. package/dist/core/surface.js.map +1 -1
  55. package/dist/lib/vfs/providers/static_content.js +10 -81
  56. package/dist/lib/vfs/providers/static_content.js.map +1 -1
  57. package/dist/lib/vfs/vfs.js +13 -1
  58. package/dist/lib/vfs/vfs.js.map +1 -1
  59. package/package.json +5 -5
@@ -0,0 +1,267 @@
1
+ /** The columns of a PACS answer's table, in reading order. */
2
+ const CSV_COLUMNS = [
3
+ 'MRN', 'PATIENT', 'SERVER', 'STATUS', 'STUDY', 'DATE',
4
+ 'ACCESSION', 'MODALITY', 'SERIES', 'ANSWERED',
5
+ ];
6
+ /**
7
+ * Quotes one cell.
8
+ *
9
+ * Everything is quoted, including numbers: a study description carrying a
10
+ * comma is the ordinary case, not the edge one, and a renderer that quotes
11
+ * only when it thinks it must is a renderer that will one day think wrong.
12
+ *
13
+ * @param value - The cell's value.
14
+ * @returns The quoted cell.
15
+ */
16
+ function cell_quote(value) {
17
+ return `"${String(value ?? '').split('"').join('""')}"`;
18
+ }
19
+ /**
20
+ * Renders one answer as CSV.
21
+ *
22
+ * ANSWERED carries the ISO timestamp rather than `3 MONTHS AGO`: a
23
+ * spreadsheet sorts and subtracts dates, and the phrase a surface reads out
24
+ * is for a human glance, not for a column of data.
25
+ *
26
+ * @param model - The answer.
27
+ * @returns The CSV text, header row first.
28
+ */
29
+ export function pacsAnswer_toCsv(model) {
30
+ const rows = [];
31
+ const patients = model.patients ?? [];
32
+ const studiesOf = (patient) => model.studies.filter((study) => study.patientId === patient.patientId
33
+ && (patient.server === undefined || study.server === undefined || study.server === patient.server));
34
+ const study_row = (study, facts) => [
35
+ facts.mrn,
36
+ study.patientName,
37
+ facts.server,
38
+ facts.status,
39
+ study.description,
40
+ study.date,
41
+ study.accession,
42
+ study.modalities,
43
+ String(study.series.length),
44
+ facts.answeredAt,
45
+ ];
46
+ if (patients.length > 0) {
47
+ for (const patient of patients) {
48
+ const server = patient.server ?? model.pacsName;
49
+ const answeredAt = patient.provenance?.answeredAt ?? '';
50
+ const owned = studiesOf(patient);
51
+ if (owned.length === 0) {
52
+ // The row that cannot be derived from studies, and the one an
53
+ // audit is usually about: a patient with no imaging, or one whose
54
+ // question could not be asked at all.
55
+ rows.push([
56
+ patient.patientId, patient.patientName ?? '', server, patient.status,
57
+ '', '', '', '', '0', answeredAt,
58
+ ]);
59
+ continue;
60
+ }
61
+ for (const study of owned) {
62
+ rows.push(study_row(study, {
63
+ mrn: patient.patientId, server, status: patient.status, answeredAt,
64
+ }));
65
+ }
66
+ }
67
+ }
68
+ else {
69
+ for (const study of model.studies) {
70
+ rows.push(study_row(study, {
71
+ mrn: study.patientId,
72
+ server: study.server ?? model.pacsName,
73
+ status: 'found',
74
+ answeredAt: model.provenance?.answeredAt ?? '',
75
+ }));
76
+ }
77
+ }
78
+ const lines = [CSV_COLUMNS.map(cell_quote).join(',')];
79
+ for (const row of rows)
80
+ lines.push(row.map(cell_quote).join(','));
81
+ return `${lines.join('\n')}\n`;
82
+ }
83
+ /**
84
+ * Paths a file cannot be written to.
85
+ *
86
+ * These are providers — commands, processes, PACS queries — not storage. A
87
+ * write into one fails inside CUBE with a message about an upload, which
88
+ * tells an operator nothing about why the place they chose was never a
89
+ * place at all.
90
+ */
91
+ const VIRTUAL_PREFIXES = ['/bin', '/usr', '/etc', '/proc', '/net'];
92
+ /** How long to wait for a removed file to actually stop resolving. */
93
+ const GONE_TIMEOUT_MS = 10_000;
94
+ /** How often to ask whether it has. */
95
+ const GONE_POLL_MS = 400;
96
+ /**
97
+ * Whether a resolved path names a provider rather than storage.
98
+ *
99
+ * @param resolved - An absolute CFS path.
100
+ * @returns True when nothing can be written there.
101
+ */
102
+ function path_isVirtual(resolved) {
103
+ return VIRTUAL_PREFIXES.some((prefix) => resolved === prefix || resolved.startsWith(`${prefix}/`));
104
+ }
105
+ /**
106
+ * Writes a rendered table into ChRIS storage.
107
+ *
108
+ * The path plumbing lives here rather than in the command: rendering a
109
+ * table and putting it somewhere are one job, and the command should read
110
+ * as the decision it makes rather than as the mechanics it performs.
111
+ *
112
+ * Imported where they are used — a CSV destination is a rare path, and
113
+ * pulling the session and storage stacks into this module's graph would
114
+ * load both for every query that never names one.
115
+ *
116
+ * The reason travels in the result rather than on the error stack, which
117
+ * keeps this module free of any runtime import — so the renderer beside it
118
+ * can be tested without loading the storage stack to do it.
119
+ *
120
+ * Four things are checked before anything is written, because a chooser
121
+ * can hand this a place that cannot be used:
122
+ *
123
+ * 1. A provider path is refused BY NAME. `/net/pacs/x.csv` fails inside
124
+ * CUBE with a message about an upload, which says nothing about why.
125
+ * 2. A missing parent is created, and the result says so — an operator
126
+ * reads what happened rather than discovering a folder later.
127
+ * 3. An existing file is never overwritten in silence: an audit table
128
+ * quietly replacing another audit table is the workaround the
129
+ * principles forbid, so it takes `--force` to say it twice.
130
+ * 4. The path that was actually written is returned, so a surface can
131
+ * state it rather than repeat what the operator typed.
132
+ *
133
+ * @param csv - The rendered table.
134
+ * @param destination - Where the operator said to put it.
135
+ * @param force - True when the operator asked to overwrite.
136
+ * @returns The resolved path it landed on, or why it did not.
137
+ */
138
+ export async function csvFile_write(csv, destination, force = false) {
139
+ try {
140
+ return await csvFile_put(csv, destination, force);
141
+ }
142
+ catch (error) {
143
+ // CUBE's failures arrive as thrown axios errors, and a throw out of a
144
+ // builtin does not stop at the command: under a daemon it takes the
145
+ // process, and every surface attached to it, with it. Measured — a
146
+ // re-upload over an existing path answers 500 and threw straight
147
+ // through this call. A store's bad day is a refusal, not an outage.
148
+ const why = error instanceof Error ? error.message : String(error);
149
+ return { ok: false, message: `${destination} could not be written: ${why}` };
150
+ }
151
+ }
152
+ /**
153
+ * The write itself, once the guard above owns the failures.
154
+ *
155
+ * @param csv - The rendered table.
156
+ * @param destination - Where the operator said to put it.
157
+ * @param force - True when the operator asked to replace what is there.
158
+ * @returns The resolved path it landed on, or why it did not.
159
+ */
160
+ async function csvFile_put(csv, destination, force) {
161
+ const { errorStack } = await import('@fnndsc/cumin');
162
+ const { path_resolve, error_stripDebugPrefix } = await import('../utils.js');
163
+ const { files_create, files_delete, files_listAll, files_mkdir, files_path_isDirectory } = await import('@fnndsc/salsa');
164
+ const resolved = await path_resolve(destination);
165
+ if (path_isVirtual(resolved)) {
166
+ return {
167
+ ok: false,
168
+ message: `${resolved} is a provider, not storage: nothing can be written there`,
169
+ };
170
+ }
171
+ const cut = resolved.lastIndexOf('/');
172
+ const parent = cut <= 0 ? '/' : resolved.slice(0, cut);
173
+ const name = resolved.slice(cut + 1);
174
+ if (name === '')
175
+ return { ok: false, message: `${resolved} names a folder, not a file` };
176
+ /**
177
+ * Whether the folder already holds a file of that name.
178
+ *
179
+ * A null listing means the folder holds no files — `files_listAll`
180
+ * answers null both for an empty folder and for one that is not there,
181
+ * so existence is asked of `files_path_isDirectory` instead and never
182
+ * inferred from this.
183
+ */
184
+ const taken_find = async () => {
185
+ const listed = await files_listAll({ limit: 1000, offset: 0 }, 'files', parent);
186
+ const rows = listed?.tableData ?? [];
187
+ return rows.find((row) => {
188
+ const candidate = row.path ?? row.fname ?? '';
189
+ return candidate === resolved || candidate.slice(candidate.lastIndexOf('/') + 1) === name;
190
+ }) ?? null;
191
+ };
192
+ let created;
193
+ let replaced = false;
194
+ if (!await files_path_isDirectory(parent)) {
195
+ // The folder the operator named is not there. Making it is the ordinary
196
+ // thing to want, so it is made — and said, since a directory appearing
197
+ // without a word is the silent side effect this replaces.
198
+ if (!await files_mkdir(parent)) {
199
+ return { ok: false, message: `${parent} does not exist and could not be created` };
200
+ }
201
+ created = parent;
202
+ }
203
+ else {
204
+ const standing = await taken_find();
205
+ if (standing !== null && !force) {
206
+ // An audit table quietly replacing another audit table is the silent
207
+ // workaround the principles forbid: it takes saying so twice.
208
+ return { ok: false, message: `${resolved} already exists; pass --force to overwrite it` };
209
+ }
210
+ if (standing !== null) {
211
+ // Removed, then written. CUBE answers a re-upload of a path it
212
+ // already holds with a 500 — measured — so "overwrite" has to be
213
+ // spelled out rather than left to the store, and the removal is
214
+ // reported because a file disappearing unremarked is the silent act
215
+ // this whole check exists to prevent.
216
+ const id = standing.id;
217
+ if (id === undefined || !await files_delete(id, 'files', parent)) {
218
+ return { ok: false, message: `${resolved} exists and could not be replaced` };
219
+ }
220
+ // Deletion in CUBE is asynchronous — salsa's folder delete documents
221
+ // the same thing — and writing into a path the store still holds
222
+ // answers 500. So the removal is waited out rather than assumed,
223
+ // which is what turned a replacement into a lost file and an error.
224
+ let gone = false;
225
+ for (let waited = 0; waited < GONE_TIMEOUT_MS; waited += GONE_POLL_MS) {
226
+ if (await taken_find() === null) {
227
+ gone = true;
228
+ break;
229
+ }
230
+ await new Promise((settle) => {
231
+ setTimeout(settle, GONE_POLL_MS);
232
+ });
233
+ }
234
+ if (!gone) {
235
+ return {
236
+ ok: false,
237
+ message: `${resolved} was removed but the store still holds it; nothing was written`,
238
+ };
239
+ }
240
+ replaced = true;
241
+ }
242
+ }
243
+ const written = await files_create(csv, resolved);
244
+ if (written) {
245
+ // Confirmed, not assumed. A create straight after making its folder has
246
+ // been seen to report success and leave nothing behind, and a table an
247
+ // operator believes they have is worse than one they know they lack.
248
+ if (await taken_find() === null) {
249
+ return {
250
+ ok: false,
251
+ message: `${resolved} was reported written but is not there; try again`,
252
+ };
253
+ }
254
+ return {
255
+ ok: true,
256
+ path: resolved,
257
+ ...(created === undefined ? {} : { created }),
258
+ ...(replaced ? { replaced: true } : {}),
259
+ };
260
+ }
261
+ const problem = errorStack.stack_pop();
262
+ // Stripped where it is read: a refusal an operator acts on should not
263
+ // arrive wearing the stack's debugging prefix.
264
+ const why = problem === undefined ? 'refused' : error_stripDebugPrefix(problem.message);
265
+ return { ok: false, message: `could not write ${resolved}: ${why}` };
266
+ }
267
+ //# sourceMappingURL=query.csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.csv.js","sourceRoot":"","sources":["../../../src/builtins/net/query.csv.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,7 @@
9
9
  * @module
10
10
  */
11
11
  import { PACSQueryDecodedResult, type CommandEnvelope } from '@fnndsc/cumin';
12
- import { type PacsQueryModel } from '@fnndsc/menu';
12
+ import { type PacsProvenance, type PacsQueryModel } from '@fnndsc/menu';
13
13
  /**
14
14
  * Result of a successful query-create-and-wait operation.
15
15
  *
@@ -65,7 +65,34 @@ export declare function pacsQueryModel_build(decoded: PACSQueryDecodedResult, fa
65
65
  vfsPath: string;
66
66
  pacsName: string;
67
67
  expression: string;
68
+ /**
69
+ * Where this answer came from. Said once here so chell's sentence and
70
+ * argus's pill are the same fact rather than two guesses at it.
71
+ */
72
+ provenance?: PacsProvenance;
68
73
  }): PacsQueryModel;
74
+ /**
75
+ * Resolves whatever names a PACS server into its canonical identifier.
76
+ *
77
+ * The context may hold a numeric id, a `--pacsserver` may be either, and
78
+ * CUBE files a query under the identifier. The index is keyed on what CUBE
79
+ * stores, so both the write and the lookup have to speak that.
80
+ *
81
+ * @param pacsserver - An id or an identifier.
82
+ * @returns The identifier, or the input unchanged when it cannot be resolved.
83
+ */
84
+ export declare function pacsIdentifier_resolve(pacsserver: string): Promise<string>;
85
+ /**
86
+ * Says how long ago something was answered, in the coarsest true unit.
87
+ *
88
+ * A replay states its age because mise refuses to decide staleness on the
89
+ * operator's behalf: an accession names a study that will not change, an
90
+ * MRN can gain one tomorrow, and no heuristic tells those apart reliably.
91
+ *
92
+ * @param at - An ISO timestamp.
93
+ * @returns A phrase like `3 months ago`, or null when the stamp is unusable.
94
+ */
95
+ export declare function age_describe(at: string): string | null;
69
96
  /**
70
97
  * Creates a PACS query, waits for results, displays findings, and prints the VFS path.
71
98
  *
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @file A PACS question that names several patients is several questions.
3
+ *
4
+ * A PACS will not match a list. `PatientID:4356325\4433255` — the DICOM
5
+ * multi-value form — returns nothing from PACSDCM, and the standard agrees:
6
+ * *List of UID Matching* is defined only for attributes whose VR is `UI`,
7
+ * and `PatientID` is `LO`. So two hundred MRNs is two hundred C-FINDs. The
8
+ * fan-out is forced, not chosen.
9
+ *
10
+ * This module is the arithmetic of that: reading a cohort out of what the
11
+ * operator typed (or out of a file in CFS), turning multi-valued terms into
12
+ * the individual questions they stand for, and refusing by name when the
13
+ * cross-product would launch hundreds.
14
+ *
15
+ * @module
16
+ */
17
+ import { type Result } from '@fnndsc/cumin';
18
+ /** The DICOM key a cohort is expressed in. */
19
+ export declare const PATIENT_KEY: string;
20
+ /**
21
+ * The most questions one inline expression may stand for.
22
+ *
23
+ * A handful of MRNs typed on a line is the case this serves; a real cohort
24
+ * belongs in `--patients`, which is why the refusal names that flag. The
25
+ * cross-product of several multi-valued terms grows fast enough that a typo
26
+ * could otherwise launch hundreds of C-FINDs at a shared clinical system.
27
+ */
28
+ export declare const QUERY_INLINE_FANOUT_MAX: number;
29
+ /**
30
+ * The most questions `--patients` may stand for.
31
+ *
32
+ * Deliberately generous — a two-hundred-MRN audit is the point of the flag —
33
+ * but not unbounded: a file of the wrong kind should be refused rather than
34
+ * walked.
35
+ */
36
+ export declare const QUERY_COHORT_MAX: number;
37
+ /**
38
+ * Parses a query expression into terms, each of which may carry several
39
+ * values.
40
+ *
41
+ * The operator's own syntax, accepted as written:
42
+ *
43
+ * ```
44
+ * PatientID:1234,4532,6654 one key, three values → three questions
45
+ * PatientID:1234,StudyDate:20240101 two keys, one value each → one question
46
+ * ```
47
+ *
48
+ * There is no ambiguity to resolve: every genuine term carries a colon, so a
49
+ * bare segment can only be another value for the key before it.
50
+ *
51
+ * @param expr - The expression as typed.
52
+ * @returns Each key with its values in order, or null when the expression is
53
+ * malformed.
54
+ */
55
+ export declare function queryTerms_parse(expr: string): Record<string, string[]> | null;
56
+ /**
57
+ * Expands multi-valued terms into the individual questions they stand for.
58
+ *
59
+ * Several multi-valued keys fan out over their cross-product, in the order
60
+ * the keys were written, so the questions come out in the order an operator
61
+ * would have listed them.
62
+ *
63
+ * @param terms - Parsed terms.
64
+ * @returns One criteria record per question.
65
+ */
66
+ export declare function queryTerms_expand(terms: Record<string, string[]>): Array<Record<string, string>>;
67
+ /**
68
+ * Reads a `--patients` value: MRNs listed inline, or `@` and a CFS path.
69
+ *
70
+ * The file is read through the session's own CFS path, never the engine's
71
+ * host filesystem, so the flag behaves identically from a local shell, a
72
+ * remote shell and a browser — and a list sitting on somebody's own machine
73
+ * reaches it through `upload`, which is the gated door, rather than through
74
+ * a second ungated one.
75
+ *
76
+ * One MRN per line; blank lines and `#` comments are ignored. An inline
77
+ * value is comma-separated, which is what a handful of MRNs looks like.
78
+ *
79
+ * @param value - The flag's value.
80
+ * @returns The MRNs in the order given, or a failure with the reason on the
81
+ * error stack.
82
+ */
83
+ export declare function patients_read(value: string): Promise<Result<string[]>>;
84
+ /**
85
+ * Guards a fan-out's size, refusing by name rather than launching it.
86
+ *
87
+ * @param count - How many questions the expansion came to.
88
+ * @param limit - The ceiling that applies to how they were asked.
89
+ * @returns True when the fan-out may proceed; otherwise the reason is on the
90
+ * error stack.
91
+ */
92
+ export declare function fanout_permit(count: number, limit: number): boolean;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @file A PACS question that names several patients is several questions.
3
+ *
4
+ * A PACS will not match a list. `PatientID:4356325\4433255` — the DICOM
5
+ * multi-value form — returns nothing from PACSDCM, and the standard agrees:
6
+ * *List of UID Matching* is defined only for attributes whose VR is `UI`,
7
+ * and `PatientID` is `LO`. So two hundred MRNs is two hundred C-FINDs. The
8
+ * fan-out is forced, not chosen.
9
+ *
10
+ * This module is the arithmetic of that: reading a cohort out of what the
11
+ * operator typed (or out of a file in CFS), turning multi-valued terms into
12
+ * the individual questions they stand for, and refusing by name when the
13
+ * cross-product would launch hundreds.
14
+ *
15
+ * @module
16
+ */
17
+ import { Err, Ok, errorStack } from '@fnndsc/cumin';
18
+ /** The DICOM key a cohort is expressed in. */
19
+ export const PATIENT_KEY = 'PatientID';
20
+ /**
21
+ * The most questions one inline expression may stand for.
22
+ *
23
+ * A handful of MRNs typed on a line is the case this serves; a real cohort
24
+ * belongs in `--patients`, which is why the refusal names that flag. The
25
+ * cross-product of several multi-valued terms grows fast enough that a typo
26
+ * could otherwise launch hundreds of C-FINDs at a shared clinical system.
27
+ */
28
+ export const QUERY_INLINE_FANOUT_MAX = 32;
29
+ /**
30
+ * The most questions `--patients` may stand for.
31
+ *
32
+ * Deliberately generous — a two-hundred-MRN audit is the point of the flag —
33
+ * but not unbounded: a file of the wrong kind should be refused rather than
34
+ * walked.
35
+ */
36
+ export const QUERY_COHORT_MAX = 500;
37
+ /** Marks a `--patients` value as naming a file rather than listing MRNs. */
38
+ const FILE_SIGIL = '@';
39
+ /**
40
+ * Parses a query expression into terms, each of which may carry several
41
+ * values.
42
+ *
43
+ * The operator's own syntax, accepted as written:
44
+ *
45
+ * ```
46
+ * PatientID:1234,4532,6654 one key, three values → three questions
47
+ * PatientID:1234,StudyDate:20240101 two keys, one value each → one question
48
+ * ```
49
+ *
50
+ * There is no ambiguity to resolve: every genuine term carries a colon, so a
51
+ * bare segment can only be another value for the key before it.
52
+ *
53
+ * @param expr - The expression as typed.
54
+ * @returns Each key with its values in order, or null when the expression is
55
+ * malformed.
56
+ */
57
+ export function queryTerms_parse(expr) {
58
+ const terms = {};
59
+ let current = null;
60
+ for (const raw of expr.split(',')) {
61
+ const part = raw.trim();
62
+ if (part === '')
63
+ return null;
64
+ const colon = part.indexOf(':');
65
+ if (colon < 1) {
66
+ // A bare segment before any key names nothing.
67
+ if (current === null)
68
+ return null;
69
+ terms[current].push(part);
70
+ continue;
71
+ }
72
+ const key = part.slice(0, colon).trim();
73
+ const value = part.slice(colon + 1).trim();
74
+ if (key === '' || value === '')
75
+ return null;
76
+ current = key;
77
+ terms[key] = [...(terms[key] ?? []), value];
78
+ }
79
+ return Object.keys(terms).length > 0 ? terms : null;
80
+ }
81
+ /**
82
+ * Expands multi-valued terms into the individual questions they stand for.
83
+ *
84
+ * Several multi-valued keys fan out over their cross-product, in the order
85
+ * the keys were written, so the questions come out in the order an operator
86
+ * would have listed them.
87
+ *
88
+ * @param terms - Parsed terms.
89
+ * @returns One criteria record per question.
90
+ */
91
+ export function queryTerms_expand(terms) {
92
+ let expanded = [{}];
93
+ for (const [key, values] of Object.entries(terms)) {
94
+ const grown = [];
95
+ for (const partial of expanded) {
96
+ for (const value of values)
97
+ grown.push({ ...partial, [key]: value });
98
+ }
99
+ expanded = grown;
100
+ }
101
+ return expanded;
102
+ }
103
+ /**
104
+ * Reads a `--patients` value: MRNs listed inline, or `@` and a CFS path.
105
+ *
106
+ * The file is read through the session's own CFS path, never the engine's
107
+ * host filesystem, so the flag behaves identically from a local shell, a
108
+ * remote shell and a browser — and a list sitting on somebody's own machine
109
+ * reaches it through `upload`, which is the gated door, rather than through
110
+ * a second ungated one.
111
+ *
112
+ * One MRN per line; blank lines and `#` comments are ignored. An inline
113
+ * value is comma-separated, which is what a handful of MRNs looks like.
114
+ *
115
+ * @param value - The flag's value.
116
+ * @returns The MRNs in the order given, or a failure with the reason on the
117
+ * error stack.
118
+ */
119
+ export async function patients_read(value) {
120
+ if (!value.startsWith(FILE_SIGIL)) {
121
+ const listed = value.split(',')
122
+ .map((entry) => entry.trim())
123
+ .filter((entry) => entry !== '');
124
+ if (listed.length === 0) {
125
+ errorStack.stack_push('error', 'query: --patients names no patients.');
126
+ return Err();
127
+ }
128
+ return Ok(listed);
129
+ }
130
+ const named = value.slice(FILE_SIGIL.length);
131
+ if (named === '') {
132
+ errorStack.stack_push('error', 'query: --patients @ names no file.');
133
+ return Err();
134
+ }
135
+ // Imported where they are used: a cohort file is a rare path, and
136
+ // pulling the session and storage stacks into this module's graph would
137
+ // load both for every query that never names one.
138
+ const { path_resolve } = await import('../utils.js');
139
+ const { fileContent_get } = await import('@fnndsc/salsa');
140
+ const resolved = await path_resolve(named);
141
+ const content = await fileContent_get(resolved);
142
+ if (!content.ok) {
143
+ const problem = errorStack.stack_pop();
144
+ errorStack.stack_push('error', `query: cannot read ${resolved}: ${problem?.message ?? 'unreadable'}`);
145
+ return Err();
146
+ }
147
+ const mrns = content.value
148
+ .split('\n')
149
+ .map((line) => line.trim())
150
+ .filter((line) => line !== '' && !line.startsWith('#'));
151
+ if (mrns.length === 0) {
152
+ errorStack.stack_push('error', `query: ${resolved} lists no patients.`);
153
+ return Err();
154
+ }
155
+ return Ok(mrns);
156
+ }
157
+ /**
158
+ * Guards a fan-out's size, refusing by name rather than launching it.
159
+ *
160
+ * @param count - How many questions the expansion came to.
161
+ * @param limit - The ceiling that applies to how they were asked.
162
+ * @returns True when the fan-out may proceed; otherwise the reason is on the
163
+ * error stack.
164
+ */
165
+ export function fanout_permit(count, limit) {
166
+ if (count <= limit)
167
+ return true;
168
+ errorStack.stack_push('error', `query: refusing: ${count} queries; use --patients with a file, or narrow the terms.`);
169
+ return false;
170
+ }
171
+ //# sourceMappingURL=query.fanout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.fanout.js","sourceRoot":"","sources":["../../../src/builtins/net/query.fanout.ts"],"names":[],"mappings":"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"}