@myelinbridge/cli 0.2.0 → 0.4.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 (3) hide show
  1. package/README.md +19 -1
  2. package/bin/myelin.js +84 -1
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -11,15 +11,33 @@ export MYELIN_API_KEY=myl_live_… # created by your bridge owner in Bridge
11
11
 
12
12
  npx @myelinbridge/cli ping # verifies auth, prints your projects
13
13
  npx @myelinbridge/cli datasets # what you can deliver to, and whose move it is
14
+ npx @myelinbridge/cli contract --dataset onco1-wes # what is expected of your delivery
15
+ npx @myelinbridge/cli sample-depth 1 --dataset onco1-wes # once, before your first submit
14
16
  npx @myelinbridge/cli check ./run_042 --dataset onco1-wes # validate BEFORE uploading a byte
15
17
  npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
16
18
  ```
17
19
 
20
+ - **Declare your sample depth once, first.** The client creates and describes the
21
+ dataset; you own your output structure, so you tell Myelin at which folder depth a
22
+ sample sits — `0` = the delivery root is one sample, `1` (default) = each top-level
23
+ folder is a sample, `2` = one level deeper. Sample-scoped quality checks group by it,
24
+ so getting it right up front is what makes per-sample verdicts mean anything. It is
25
+ **idempotent** (safe to assert on every pipeline run) and **locks once your first
26
+ batch leaves draft**, so that verdicts stay comparable across deliveries — after that
27
+ the command exits `2`. This is the only dataset field you can write.
28
+
18
29
  - **Resume = re-run.** `push` is idempotent: already-uploaded files are skipped
19
30
  (path + size), and within a large file, parts that already landed are skipped
20
31
  too (S3 multipart). Uploads go direct to storage over short-lived presigned
21
32
  URLs — no credential is stored on your machine, and revoking the API key cuts
22
33
  off signing immediately.
34
+ - **Read the contract before you build the delivery.** `contract` prints what the
35
+ client expects — every check as one plain sentence, grouped by what it answers
36
+ (completeness, structure, validity, consistency, integrity, privacy), and marked
37
+ `!` when a failure blocks validation. It also tells you which checks `check` can
38
+ verify locally and which only run once the files are uploaded, so nothing about
39
+ the bar is a surprise at review time.
40
+
23
41
  - **`check` costs nothing.** It evaluates your local manifest against the dataset's
24
42
  quality checks server-side — same engine, same verdicts as submit — without
25
43
  uploading. Exit code 2 means a blocking rule fails.
@@ -31,7 +49,7 @@ npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submit
31
49
  ## Machine mode
32
50
 
33
51
  Every command takes `--json`. Exit codes: `0` ok · `1` error · `2` blocked
34
- (blocking preflight failure, locked delivery, blocked submit).
52
+ (blocking preflight failure, locked delivery, blocked submit, locked sample depth).
35
53
 
36
54
  ## Webhooks instead of polling
37
55
 
package/bin/myelin.js CHANGED
@@ -54,7 +54,7 @@ function expectOk(r, context) {
54
54
  if (r.status >= 200 && r.status < 300) return r.json
55
55
  const detail = r.json?.detail ?? `HTTP ${r.status}`
56
56
  const code = r.json?.code ?? 'error'
57
- const blocked = ['delivery_locked', 'submit_blocked', 'dataset_not_active', 'api_disabled', 'bridge_paused'].includes(code)
57
+ const blocked = ['delivery_locked', 'submit_blocked', 'dataset_not_active', 'api_disabled', 'bridge_paused', 'sample_depth_locked'].includes(code)
58
58
  die(`${context}: [${code}] ${detail}`, blocked ? 2 : 1)
59
59
  }
60
60
 
@@ -207,6 +207,60 @@ async function cmdDatasets() {
207
207
  }
208
208
  }
209
209
 
210
+ // The delivery contract, before you deliver anything. Answers "what is expected
211
+ // of me?" — which used to be discoverable only by submitting and being rejected.
212
+ async function cmdContract() {
213
+ const dsRef = opt('dataset')
214
+ if (!dsRef) die('Usage: myelin contract --dataset <slug|id>')
215
+ const dataset = await resolveDataset(dsRef)
216
+ const j = expectOk(
217
+ await api('GET', `/datasets/${dataset.id}/quality-checks`),
218
+ 'quality-checks',
219
+ )
220
+ emit(j)
221
+ if (JSON_MODE) return
222
+
223
+ if (!j.version) {
224
+ out(`${dataset.name}: no quality contract published yet — nothing is enforced.`)
225
+ return
226
+ }
227
+
228
+ const s = j.summary
229
+ out(`${dataset.name} — delivery contract v${j.version}`)
230
+ out(
231
+ `${s.total} checks · ${s.blocking} block validation · ` +
232
+ `${s.checkable_before_upload} checkable before upload` +
233
+ (s.needs_reviewer ? ` · ${s.needs_reviewer} reviewed by a person` : ''),
234
+ )
235
+ out('')
236
+
237
+ // Grouped by dimension so the contract reads as questions, not a flat list.
238
+ const byDim = new Map()
239
+ for (const c of j.checks) {
240
+ const key = c.dimension ?? 'review'
241
+ if (!byDim.has(key)) byDim.set(key, [])
242
+ byDim.get(key).push(c)
243
+ }
244
+ for (const d of s.dimensions) {
245
+ const items = byDim.get(d.key) ?? []
246
+ if (items.length === 0) continue
247
+ out(`${d.label.toUpperCase()} — ${d.question}`)
248
+ for (const c of items) {
249
+ const gate = c.severity === 'blocking' ? 'must' : 'should'
250
+ const when = c.runs_at === 'preflight' ? '' : ' (checked at submission)'
251
+ out(` ${gate === 'must' ? '!' : '·'} ${c.assertion ?? c.name}${when}`)
252
+ }
253
+ out('')
254
+ }
255
+ const manual = byDim.get('review') ?? []
256
+ if (manual.length > 0) {
257
+ out('REVIEWER JUDGEMENT — decided by a person, not the engine')
258
+ for (const c of manual) out(` · ${c.name}`)
259
+ out('')
260
+ }
261
+ out(`Run "myelin check <dir> --dataset ${dsRef}" to test ${s.checkable_before_upload} of these locally.`)
262
+ }
263
+
210
264
  async function cmdCheck() {
211
265
  const dir = args[1]
212
266
  const dsRef = opt('dataset')
@@ -372,6 +426,31 @@ async function cmdSandbox() {
372
426
  out(`✓ Sandbox upload succeeded — the partner-side test transfer checklist item is satisfied for bridge "${me.bridge.name}".`)
373
427
  }
374
428
 
429
+ async function cmdSampleDepth() {
430
+ const dsRef = opt('dataset')
431
+ const raw = args[1]
432
+ if (!dsRef || raw === undefined) {
433
+ die('Usage: myelin sample-depth <0-5> --dataset <slug|id>')
434
+ }
435
+ const depth = Number(raw)
436
+ if (!Number.isInteger(depth) || depth < 0 || depth > 5) {
437
+ die('sample-depth must be an integer between 0 and 5')
438
+ }
439
+ const dataset = await resolveDataset(dsRef)
440
+ const j = expectOk(
441
+ await api('POST', `/datasets/${dataset.id}/sample-depth`, { sample_depth: depth }),
442
+ 'sample-depth',
443
+ )
444
+ emit(j)
445
+ if (!JSON_MODE) {
446
+ out(
447
+ j.changed
448
+ ? `Sample depth for ${dataset.name} set to ${j.sample_depth}.`
449
+ : `Sample depth for ${dataset.name} already ${j.sample_depth} — nothing to change.`,
450
+ )
451
+ }
452
+ }
453
+
375
454
  function cmdHelp() {
376
455
  console.log(`myelin — Partner Ingestion CLI
377
456
 
@@ -383,6 +462,8 @@ Commands:
383
462
  ping verify the key; show bridge + projects
384
463
  projects list scoped projects
385
464
  datasets list datasets with "your move" hints
465
+ sample-depth <0-5> --dataset <slug|id> declare the folder depth a sample sits at (locks after 1st submit)
466
+ contract --dataset <slug|id> what this dataset expects of your delivery
386
467
  check <dir> --dataset <slug|id> preflight local files against quality rules (no upload)
387
468
  push <dir> --dataset <slug|id> create/resume a delivery and upload (resumable; re-run to resume)
388
469
  [--submit] [--replace]
@@ -403,6 +484,8 @@ const commands = {
403
484
  ping: cmdPing,
404
485
  projects: cmdProjects,
405
486
  datasets: cmdDatasets,
487
+ 'sample-depth': cmdSampleDepth,
488
+ contract: cmdContract,
406
489
  check: cmdCheck,
407
490
  push: cmdPush,
408
491
  status: cmdStatus,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@myelinbridge/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
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": {
7
- "myelin": "./bin/myelin.js"
7
+ "myelin": "bin/myelin.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/",