@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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/bin/myelin.js +82 -7
  3. 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
- if (!dir || !dsRef) die('Usage: myelin check <dir> --dataset <slug|id>')
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.check_type.length),
594
- ...j.deferred.map((d) => d.check_type.length),
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.check_type.padEnd(w)} ${verdictLabel(c.verdict)}${gate}`)
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.check_type.padEnd(w)} ${d.reason}`)
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
- if (!dir || !dsRef) die('Usage: myelin push <dir> --dataset <slug|id> [--submit] [--replace]')
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
- if (j.project) out(` project ${j.project.code} ${j.project.title}`)
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.11.1",
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
+ }