@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.
- package/README.md +19 -1
- package/bin/myelin.js +84 -1
- 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.
|
|
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": "
|
|
7
|
+
"myelin": "bin/myelin.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|