@myelinbridge/cli 0.12.0 → 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 CHANGED
@@ -45,12 +45,15 @@ npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
45
45
 
46
46
  - **Your client's own fields show up, read-only (0.12.0).** Clients can define
47
47
  their own metadata fields on datasets (a sponsor study code, a work order, a
48
- therapeutic area). `contract` prints them under CLIENT FIELDS, and `check`
49
- tells you when required ones are not filled in yet — informational only:
50
- their team fills them in Myelin, there is nothing to change in your delivery,
51
- and metadata never moves the exit code. In `--json`, datasets carry a
52
- `metadata` object (`{key: {label, type, value}}`) and preflight carries
53
- `metadata.missing_required`.
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`.
54
57
 
55
58
  - **A missing path in `roles set` is refused, not guessed (0.11.1).** Flags are
56
59
  not positional arguments: `roles set samplesheet --dataset onco1-wes` (path
package/bin/myelin.js CHANGED
@@ -23,6 +23,23 @@
23
23
  // client has not filled in yet — informational ONLY: metadata never moves the
24
24
  // exit code (the client fills these in Myelin; nothing for a partner to fix,
25
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.
26
43
 
27
44
  import { readdirSync, statSync, createReadStream, readFileSync } from 'node:fs'
28
45
  import { resolve, join, relative, sep, basename } from 'node:path'
@@ -582,7 +599,17 @@ async function cmdContract() {
582
599
  const metaKeys = Object.keys(meta)
583
600
  if (metaKeys.length > 0) {
584
601
  out('CLIENT FIELDS — how your client describes this dataset (read-only)')
585
- for (const k of metaKeys) out(` · ${meta[k].label}: ${fmtFieldValue(meta[k].value)}`)
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
+ }
586
613
  out('')
587
614
  }
588
615
  out(`Run "myelin check <dir> --dataset ${dsRef}" to test ${s.checkable_before_upload} of these locally.`)
@@ -591,7 +618,10 @@ async function cmdContract() {
591
618
  async function cmdCheck() {
592
619
  const dir = args[1]
593
620
  const dsRef = opt('dataset')
594
- 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>')
595
625
  const root = resolve(dir)
596
626
  const files = walkDir(root)
597
627
  if (files.length === 0) die(`No files under ${root}`)
@@ -608,10 +638,15 @@ async function cmdCheck() {
608
638
  if (!JSON_MODE) {
609
639
  // One width across all three lists so the verdicts line up, and the hint
610
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.
611
646
  const w = Math.max(
612
647
  0,
613
- ...j.evaluated.map((c) => c.check_type.length),
614
- ...j.deferred.map((d) => d.check_type.length),
648
+ ...j.evaluated.map((c) => c.name.length),
649
+ ...j.deferred.map((d) => d.name.length),
615
650
  ...j.manual.map((m) => m.name.length),
616
651
  )
617
652
  for (const c of j.evaluated) {
@@ -620,7 +655,7 @@ async function cmdCheck() {
620
655
  : c.severity === 'blocking' && c.verdict === 'not_evaluated' ? ' — BLOCKING, and nobody looked'
621
656
  : c.severity === 'must_acknowledge' && c.verdict === 'failed' ? ' — your reviewer confirms this'
622
657
  : ''
623
- 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}`)
624
659
  // Why we could not look, in the partner's terms. `abstained: 'rule'` is
625
660
  // the one that matters most: it says the fault is in the client's rule,
626
661
  // not in this delivery, and without the sentence the partner spends a
@@ -644,7 +679,7 @@ async function cmdCheck() {
644
679
  (c.verdict === 'not_evaluated' && c.details?.abstained !== 'rule')
645
680
  if (fixable && c.remediation) out(` hint: ${c.remediation}`)
646
681
  }
647
- 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}`)
648
683
  for (const m of j.manual) out(`○ ${m.name.padEnd(w)} ${m.reason}`)
649
684
  }
650
685
 
@@ -716,7 +751,10 @@ async function cmdCheck() {
716
751
  async function cmdPush() {
717
752
  const dir = args[1]
718
753
  const dsRef = opt('dataset')
719
- 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]')
720
758
  const root = resolve(dir)
721
759
  const files = walkDir(root)
722
760
  if (files.length === 0) die(`No files under ${root}`)
@@ -1077,7 +1115,14 @@ async function cmdResolve() {
1077
1115
  const j = expectOk(await api('GET', `/deliveries/resolve?path=${encodeURIComponent(path)}`), 'resolve')
1078
1116
  emit(j)
1079
1117
  out(`${j.resolved} · ${j.query}`)
1080
- 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
+ }
1081
1126
  if (j.dataset) out(` dataset ${j.dataset.name}${j.dataset.label ? ` (${j.dataset.label})` : ''}`)
1082
1127
  if (j.delivery) {
1083
1128
  out(` batch ${j.delivery.batch_name ?? j.delivery.batch_id} · #${j.delivery.sequence_number ?? '?'}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myelinbridge/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
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
5
  "type": "module",
6
6
  "bin": {