@myelinbridge/cli 0.11.1 → 0.12.2
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 +12 -0
- package/bin/myelin.js +82 -7
- package/package.json +23 -23
package/README.md
CHANGED
|
@@ -43,6 +43,18 @@ npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
|
|
|
43
43
|
every later delivery reconciles against it — a partial delivery (93 of 96) is
|
|
44
44
|
reported as a state, never as a failure.
|
|
45
45
|
|
|
46
|
+
- **Your client's own fields show up, read-only (0.12.0).** Clients can define
|
|
47
|
+
their own metadata fields on datasets (a sponsor study code, a work order, a
|
|
48
|
+
therapeutic area). `contract` prints them under CLIENT FIELDS — including a
|
|
49
|
+
required field nobody has filled in yet, shown as *"— required, not filled
|
|
50
|
+
in yet"* (0.12.2) — and `check` tells you the same thing via preflight;
|
|
51
|
+
informational only: their team fills them in Myelin, there is nothing to
|
|
52
|
+
change in your delivery, and metadata never moves the exit code. In
|
|
53
|
+
`--json`, datasets carry a `metadata` object
|
|
54
|
+
(`{key: {label, type, value, required?}}` — `required: true` and
|
|
55
|
+
`value: null` mark a required field with no value yet) and preflight
|
|
56
|
+
carries `metadata.missing_required`.
|
|
57
|
+
|
|
46
58
|
- **A missing path in `roles set` is refused, not guessed (0.11.1).** Flags are
|
|
47
59
|
not positional arguments: `roles set samplesheet --dataset onco1-wes` (path
|
|
48
60
|
omitted) used to declare the samplesheet at the literal path `--dataset` and
|
package/bin/myelin.js
CHANGED
|
@@ -17,6 +17,29 @@
|
|
|
17
17
|
// cannot validate either without deciding by hand, and hearing "all checks
|
|
18
18
|
// pass" before uploading is the green-before-sending / red-after trap this
|
|
19
19
|
// release exists to close.
|
|
20
|
+
//
|
|
21
|
+
// 0.12.0 — clients can define their own fields on datasets (sprint 22).
|
|
22
|
+
// `contract` shows them read-only, and `check` reports the required ones the
|
|
23
|
+
// client has not filled in yet — informational ONLY: metadata never moves the
|
|
24
|
+
// exit code (the client fills these in Myelin; nothing for a partner to fix,
|
|
25
|
+
// and a pipeline must never halt on it).
|
|
26
|
+
//
|
|
27
|
+
// 0.12.1 — three display-only fixes, no behavior change. `check`'s
|
|
28
|
+
// human-readable output now labels every rule by `name` instead of
|
|
29
|
+
// `check_type`: the type is the underlying evaluator (many-to-one with
|
|
30
|
+
// rules), so two rules sharing one used to print identical, contradictory
|
|
31
|
+
// lines. `check`/`push` now refuse an omitted <dir> the same way `roles set`
|
|
32
|
+
// already refused an omitted <path> (0.11.1) — before, the following
|
|
33
|
+
// --dataset flag was silently swallowed as the directory and the command
|
|
34
|
+
// died with a raw ENOENT instead of its usage line. `resolve` no longer
|
|
35
|
+
// doubles a project's code ("AML-CART-3B — AML-CART-3B — Phase 3…") when the
|
|
36
|
+
// title already starts with it.
|
|
37
|
+
//
|
|
38
|
+
// 0.12.2 — a required field with no value yet used to be invisible in
|
|
39
|
+
// `contract` (it only ever rendered keys the dataset already had a value
|
|
40
|
+
// for), even though `check` correctly warned about it via preflight. The API
|
|
41
|
+
// now includes required-but-empty fields in `metadata` too
|
|
42
|
+
// (`required: true`, `value: null`), and `contract` renders them.
|
|
20
43
|
|
|
21
44
|
import { readdirSync, statSync, createReadStream, readFileSync } from 'node:fs'
|
|
22
45
|
import { resolve, join, relative, sep, basename } from 'node:path'
|
|
@@ -102,6 +125,11 @@ const VERDICT_LABEL = {
|
|
|
102
125
|
const verdictMark = (v) => VERDICT_MARK[v] ?? '?'
|
|
103
126
|
const verdictLabel = (v) => VERDICT_LABEL[v] ?? v
|
|
104
127
|
|
|
128
|
+
// A client field value in human mode (0.12.0). Arrays joined, booleans said
|
|
129
|
+
// in words, everything else printed as-is — never JSON punctuation.
|
|
130
|
+
const fmtFieldValue = (v) =>
|
|
131
|
+
Array.isArray(v) ? v.join(', ') : v === true ? 'yes' : v === false ? 'no' : String(v ?? '—')
|
|
132
|
+
|
|
105
133
|
// Column widths come from the content. padEnd() alone silently ran a long
|
|
106
134
|
// value into the next column — a 28-character dataset slug swallowed the
|
|
107
135
|
// STATUS header's gutter. Pass headers = null for an unheadered list.
|
|
@@ -565,13 +593,35 @@ async function cmdContract() {
|
|
|
565
593
|
for (const c of manual) out(` · ${c.name}`)
|
|
566
594
|
out('')
|
|
567
595
|
}
|
|
596
|
+
// 0.12.0 — the client's own fields on this dataset, read-only (their team
|
|
597
|
+
// writes them in Myelin; the API never accepts them from a partner key).
|
|
598
|
+
const meta = dataset.metadata ?? {}
|
|
599
|
+
const metaKeys = Object.keys(meta)
|
|
600
|
+
if (metaKeys.length > 0) {
|
|
601
|
+
out('CLIENT FIELDS — how your client describes this dataset (read-only)')
|
|
602
|
+
for (const k of metaKeys) {
|
|
603
|
+
const f = meta[k]
|
|
604
|
+
// A required field with no value yet is invisible unless we say so
|
|
605
|
+
// here too — `myelin check` already warns about it via preflight, and
|
|
606
|
+
// the two must agree on what's outstanding (0.12.0, cli-api-18).
|
|
607
|
+
out(
|
|
608
|
+
f.required
|
|
609
|
+
? ` · ${f.label} — required, not filled in yet`
|
|
610
|
+
: ` · ${f.label}: ${fmtFieldValue(f.value)}`,
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
out('')
|
|
614
|
+
}
|
|
568
615
|
out(`Run "myelin check <dir> --dataset ${dsRef}" to test ${s.checkable_before_upload} of these locally.`)
|
|
569
616
|
}
|
|
570
617
|
|
|
571
618
|
async function cmdCheck() {
|
|
572
619
|
const dir = args[1]
|
|
573
620
|
const dsRef = opt('dataset')
|
|
574
|
-
|
|
621
|
+
// Same trap as `roles set` (see the comment there): an omitted <dir> lets
|
|
622
|
+
// the following --dataset flag slide into its place, which used to reach
|
|
623
|
+
// resolve()/walkDir() and fail with a raw ENOENT instead of usage.
|
|
624
|
+
if (!dir || dir.startsWith('--') || !dsRef) die('Usage: myelin check <dir> --dataset <slug|id>')
|
|
575
625
|
const root = resolve(dir)
|
|
576
626
|
const files = walkDir(root)
|
|
577
627
|
if (files.length === 0) die(`No files under ${root}`)
|
|
@@ -588,10 +638,15 @@ async function cmdCheck() {
|
|
|
588
638
|
if (!JSON_MODE) {
|
|
589
639
|
// One width across all three lists so the verdicts line up, and the hint
|
|
590
640
|
// lines can still be interleaved under the check they belong to.
|
|
641
|
+
// Labelled by `name`, not `check_type` — the type is the underlying
|
|
642
|
+
// evaluator (e.g. "manifest_present") and is many-to-one with rules, so
|
|
643
|
+
// two rules of the same type used to print identical, contradictory
|
|
644
|
+
// lines. `name` is unique per rule and is what the manual list below
|
|
645
|
+
// already used.
|
|
591
646
|
const w = Math.max(
|
|
592
647
|
0,
|
|
593
|
-
...j.evaluated.map((c) => c.
|
|
594
|
-
...j.deferred.map((d) => d.
|
|
648
|
+
...j.evaluated.map((c) => c.name.length),
|
|
649
|
+
...j.deferred.map((d) => d.name.length),
|
|
595
650
|
...j.manual.map((m) => m.name.length),
|
|
596
651
|
)
|
|
597
652
|
for (const c of j.evaluated) {
|
|
@@ -600,7 +655,7 @@ async function cmdCheck() {
|
|
|
600
655
|
: c.severity === 'blocking' && c.verdict === 'not_evaluated' ? ' — BLOCKING, and nobody looked'
|
|
601
656
|
: c.severity === 'must_acknowledge' && c.verdict === 'failed' ? ' — your reviewer confirms this'
|
|
602
657
|
: ''
|
|
603
|
-
out(`${verdictMark(c.verdict)} ${c.
|
|
658
|
+
out(`${verdictMark(c.verdict)} ${c.name.padEnd(w)} ${verdictLabel(c.verdict)}${gate}`)
|
|
604
659
|
// Why we could not look, in the partner's terms. `abstained: 'rule'` is
|
|
605
660
|
// the one that matters most: it says the fault is in the client's rule,
|
|
606
661
|
// not in this delivery, and without the sentence the partner spends a
|
|
@@ -624,7 +679,7 @@ async function cmdCheck() {
|
|
|
624
679
|
(c.verdict === 'not_evaluated' && c.details?.abstained !== 'rule')
|
|
625
680
|
if (fixable && c.remediation) out(` hint: ${c.remediation}`)
|
|
626
681
|
}
|
|
627
|
-
for (const d of j.deferred) out(`… ${d.
|
|
682
|
+
for (const d of j.deferred) out(`… ${d.name.padEnd(w)} ${d.reason}`)
|
|
628
683
|
for (const m of j.manual) out(`○ ${m.name.padEnd(w)} ${m.reason}`)
|
|
629
684
|
}
|
|
630
685
|
|
|
@@ -662,6 +717,16 @@ async function cmdCheck() {
|
|
|
662
717
|
if (j.must_acknowledge_failures > 0) {
|
|
663
718
|
out(`${j.must_acknowledge_failures} check(s) your client asked to be told about — nothing to fix; their reviewer records a decision. Not a blocker.`)
|
|
664
719
|
}
|
|
720
|
+
// 0.12.0 — required client fields not filled in yet. Informational ONLY:
|
|
721
|
+
// their team fills these in Myelin, nothing changes in the delivery, and
|
|
722
|
+
// metadata never moves the exit code (warn, never block — by design).
|
|
723
|
+
const gaps = j.metadata?.missing_required ?? []
|
|
724
|
+
if (gaps.length > 0) {
|
|
725
|
+
out(
|
|
726
|
+
`${gaps.length} field(s) your client asks for on this dataset ${gaps.length === 1 ? 'is' : 'are'} ` +
|
|
727
|
+
`not filled in yet: ${gaps.map((g) => g.label).join(', ')}. Their team fills these in Myelin — not a blocker.`,
|
|
728
|
+
)
|
|
729
|
+
}
|
|
665
730
|
|
|
666
731
|
const k = j.counts
|
|
667
732
|
// "All checks that run before upload pass" was written when every check
|
|
@@ -686,7 +751,10 @@ async function cmdCheck() {
|
|
|
686
751
|
async function cmdPush() {
|
|
687
752
|
const dir = args[1]
|
|
688
753
|
const dsRef = opt('dataset')
|
|
689
|
-
|
|
754
|
+
// Same trap as `roles set` (see the comment there): an omitted <dir> lets
|
|
755
|
+
// the following --dataset flag slide into its place, which used to reach
|
|
756
|
+
// resolve()/walkDir() and fail with a raw ENOENT instead of usage.
|
|
757
|
+
if (!dir || dir.startsWith('--') || !dsRef) die('Usage: myelin push <dir> --dataset <slug|id> [--submit] [--replace]')
|
|
690
758
|
const root = resolve(dir)
|
|
691
759
|
const files = walkDir(root)
|
|
692
760
|
if (files.length === 0) die(`No files under ${root}`)
|
|
@@ -1047,7 +1115,14 @@ async function cmdResolve() {
|
|
|
1047
1115
|
const j = expectOk(await api('GET', `/deliveries/resolve?path=${encodeURIComponent(path)}`), 'resolve')
|
|
1048
1116
|
emit(j)
|
|
1049
1117
|
out(`${j.resolved} · ${j.query}`)
|
|
1050
|
-
|
|
1118
|
+
// Some seeded/client titles already embed the project code
|
|
1119
|
+
// ("AML-CART-3B — Phase 3 CAR-T…") — only prepend it when the title
|
|
1120
|
+
// doesn't already start with it, or the code prints twice.
|
|
1121
|
+
if (j.project) {
|
|
1122
|
+
const title =
|
|
1123
|
+
j.project.title && j.project.title.startsWith(j.project.code) ? j.project.title : `${j.project.code} — ${j.project.title}`
|
|
1124
|
+
out(` project ${title}`)
|
|
1125
|
+
}
|
|
1051
1126
|
if (j.dataset) out(` dataset ${j.dataset.name}${j.dataset.label ? ` (${j.dataset.label})` : ''}`)
|
|
1052
1127
|
if (j.delivery) {
|
|
1053
1128
|
out(` batch ${j.delivery.batch_name ?? j.delivery.batch_id} · #${j.delivery.sequence_number ?? '?'}`)
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@myelinbridge/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Myelin Partner Ingestion CLI — push R&D data deliveries from a pipeline: preflight against the client's quality rules, resumable upload, submit, track review outcomes.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"myelin": "bin/myelin.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin/",
|
|
11
|
-
"README.md"
|
|
12
|
-
],
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=20"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"myelin",
|
|
18
|
-
"pharma",
|
|
19
|
-
"ingestion",
|
|
20
|
-
"s3"
|
|
21
|
-
],
|
|
22
|
-
"license": "UNLICENSED"
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@myelinbridge/cli",
|
|
3
|
+
"version": "0.12.2",
|
|
4
|
+
"description": "Myelin Partner Ingestion CLI — push R&D data deliveries from a pipeline: preflight against the client's quality rules, resumable upload, submit, track review outcomes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"myelin": "bin/myelin.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"myelin",
|
|
18
|
+
"pharma",
|
|
19
|
+
"ingestion",
|
|
20
|
+
"s3"
|
|
21
|
+
],
|
|
22
|
+
"license": "UNLICENSED"
|
|
23
|
+
}
|