@jbrowse/cli 5.0.0-beta.4 → 5.0.0-beta.8

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
@@ -427,6 +427,13 @@ naming a trackId that does not exist, a duplicate trackId. Exits 1.
427
427
  Types registered by plugins are not known to this command, so they come through
428
428
  as warnings rather than errors.
429
429
 
430
+ The same checks are published as a JSON Schema at
431
+ https://jbrowse.org/jb2/schema/v5/config.json: a "$schema" line naming it at the
432
+ top of a config.json gives an editor completion and validation for every slot.
433
+ This command runs that schema first, then the cross-references a schema cannot
434
+ express — a trackId a session names that no track defines, an assembly a track
435
+ names that none defines.
436
+
430
437
  Examples:
431
438
 
432
439
  # check the config.json in the current directory
@@ -56,9 +56,9 @@ export function mergeDisplayDefaults({ configObj, color, height, displayDefaults
56
56
  // An unknown track type accepts everything: it is a plugin's, and this manifest
57
57
  // is only the core plugins'.
58
58
  //
59
- // The message names the display when the track offers one — which most do, and
60
- // AlignmentsTrack is the case that matters here, since `--color` is the
61
- // reachable mistake and LinearAlignmentsDisplay is the only place to go look.
59
+ // The message names the display where the track offers exactly one, and the
60
+ // track otherwise: the keys checked are the union over its displays, so
61
+ // naming one of several would send the reader to look in the wrong place.
62
62
  function checkDisplayDefaultKeys(merged, trackType) {
63
63
  const { displayTypes, keys } = displayDefaultsForTrackType(trackType);
64
64
  if (keys.length === 0) {
@@ -125,7 +125,7 @@ export async function run(args) {
125
125
  throw new Error(`PIF sort/index pipeline exited with ${describeExit(exit)}`);
126
126
  }
127
127
  const { stats } = pass;
128
- const { samples, rows, skipped } = stats;
128
+ const { samples, rows, cigarRows, skipped } = stats;
129
129
  if (rows === 0) {
130
130
  discardPart();
131
131
  throw new Error(`No valid PAF rows found in ${file ?? 'stdin'} (${skipped} line(s) had fewer than the 12 mandatory PAF columns). Is this a PAF file?`);
@@ -135,6 +135,11 @@ export async function run(args) {
135
135
  if (skipped > 0) {
136
136
  console.warn(`Warning: skipped ${skipped} of ${rows + skipped} line(s) with fewer than the 12 mandatory PAF columns`);
137
137
  }
138
+ if (coarseGap !== undefined && cigarRows === 0) {
139
+ console.warn('Warning: no row carried a CIGAR (cg:Z: or cs:Z:), so the coarse tier repeats ' +
140
+ 'the fine tier and doubles the file for nothing. Rebuild with --no-coarse, ' +
141
+ 'or align with a CIGAR (minimap2 -c) so low zooms have something cheaper to read.');
142
+ }
138
143
  // An all-vs-all PAF that does not state every pair indexes perfectly well and
139
144
  // then draws an empty synteny band for each pair the aligner never emitted,
140
145
  // which reads as "no homology here" rather than as "never computed". This is
@@ -5,27 +5,31 @@ import { Transform } from 'node:stream';
5
5
  import { pipeline } from 'node:stream/promises';
6
6
  import { createGunzip } from 'node:zlib';
7
7
  import { coarsenCigar, csToCigar, flipCigar, flipCoarseCigar, swapCoarseCigar, swapIndelCigar, } from '@jbrowse/cigar-utils';
8
+ import { version } from '../../version.js';
8
9
  // Default gap (bp) of the coarse tier's coarse CIGAR: how far a straight line
9
10
  // across one of its runs may be from the alignment's real path. Indels longer
10
11
  // than half of it keep their letter; shorter ones fold into the runs. 10kb
11
12
  // matches the adapter's default coarseBpPerPxThreshold, so at the zoom where the
12
13
  // tier is served the bound is ~1px and a folded indel was sub-pixel anyway.
13
14
  export const DEFAULT_COARSE_GAP = 10_000;
15
+ // Generation 2 (2026-09): every row carries `pi:i:`, the input row's index,
16
+ // and the header names the writer.
17
+ export const PIF_FORMAT_VERSION = 2;
14
18
  /**
15
19
  * The one meta line a PIF carries, sorted first by the C-locale sort and kept
16
- * by tabix as a header. It states the format generation, the tiers written,
17
- * the coarse tier's accuracy bound (`--coarse`), and whether every input row
18
- * had a CIGAR — the facts a reader cannot recover from the rows.
20
+ * by tabix as a header. It states the format generation, the writer, the
21
+ * tiers written, the coarse tier's accuracy bound (`--coarse`), and whether
22
+ * every input row had a CIGAR — the facts a reader cannot recover from the rows.
19
23
  */
20
24
  export function pifHeader(coarseGap, stats) {
21
25
  const cigars = stats.cigarRows === 0
22
26
  ? 'none'
23
- : stats.cigarRows === stats.rows && stats.unboundedRows === 0
27
+ : stats.cigarRows === stats.rows
24
28
  ? 'all'
25
29
  : 'some';
26
30
  const tiers = coarseGap === undefined ? 'fine' : 'fine,coarse';
27
31
  const bound = coarseGap === undefined ? '' : `\tcoarse:i:${coarseGap}`;
28
- return `#pif\tversion:i:1\ttiers:Z:${tiers}${bound}\tcigars:Z:${cigars}\n`;
32
+ return `#pif\tversion:i:${PIF_FORMAT_VERSION}\twriter:Z:jbrowse-cli/${version}\ttiers:Z:${tiers}${bound}\tcigars:Z:${cigars}\n`;
29
33
  }
30
34
  function panSNSample(refName) {
31
35
  const i = refName.indexOf('#');
@@ -62,33 +66,23 @@ export function missingPairs({ samples, pairs }) {
62
66
  }
63
67
  return out;
64
68
  }
65
- // The coarse row's `cr:Z:` value, or nothing, and whether withholding it left
66
- // the row making a claim it cannot keep.
67
- //
68
- // `ops` is nothing when the row has no CIGAR, when the fold is a single run
69
- // (the coordinate columns already say it all), and when the walk does not close
70
- // on the row's own far corner — clipping ops, a hand-made cg, a cs whose spans
71
- // don't add up — since the columns are what the fine tier draws and the coarse
72
- // row must not disagree with them. A fold of several runs with no kept indel is
73
- // still written: the runs are where a lopsided cluster of small indels bends
74
- // the path, which a straight ribbon across the row would miss by up to the
75
- // whole cluster.
76
- //
77
- // `unbounded` separates the last of those from the rest. The first two leave a
78
- // tagless row a reader may take as one run within `--coarse`, and it is one; a
79
- // walk that misses its own corner leaves a row that reads the same way and is
80
- // not, so the header has to stop saying `cigars:Z:all` on its account.
69
+ // The coarse row's `cr:Z:` value, or nothing when the row has no CIGAR or the
70
+ // fold is a single run that closes on the coordinate columns, which then say it
71
+ // all. A fold of several runs with no kept indel is still written: a lopsided
72
+ // cluster of small indels bends the path by their sum. So is a fold whose walk
73
+ // misses the row's own far corner (clipping ops, a hand-made cg, a cs whose
74
+ // spans don't add up): the fine tier walks that CIGAR from the start corner
75
+ // without checking, and the coarse tier draws the same path only if it walks
76
+ // the same fold.
81
77
  function coarseFold({ cigar, coarseGap, ownLen, mateLen, }) {
82
- const coarse = cigar === undefined ? undefined : coarsenCigar(cigar, coarseGap);
83
- const closed = coarse !== undefined &&
84
- coarse.ownLen === ownLen &&
85
- coarse.mateLen === mateLen;
86
- return {
87
- ops: closed && (coarse.gapCount > 0 || coarse.opCount > 1)
88
- ? coarse.ops
89
- : undefined,
90
- unbounded: cigar !== undefined && !closed,
91
- };
78
+ if (cigar === undefined) {
79
+ return undefined;
80
+ }
81
+ const coarse = coarsenCigar(cigar, coarseGap);
82
+ const closed = coarse.ownLen === ownLen && coarse.mateLen === mateLen;
83
+ return !closed || coarse.gapCount > 0 || coarse.opCount > 1
84
+ ? coarse.ops
85
+ : undefined;
92
86
  }
93
87
  // A PIF row carries exactly ONE alignment string — `cg:Z:`, in the orientation
94
88
  // of the perspective it is indexed under. A minimap2 `cs:Z:` is folded into that
@@ -135,10 +129,14 @@ function processLine(rawLine, coarseGap, stats) {
135
129
  // rest[0]=num_matches, rest[1]=block_len, rest[2]=mapq, rest[3+]=optional tags
136
130
  const [c1, l1, s1, e1, strand, c2, l2, s2, e2, ...rest] = parts;
137
131
  addPanSNPair(stats, c1, c2);
138
- // an incoming cr:Z: (a PIF turned back into PAF, or a tool that adopted the
139
- // tag) is dropped from both tiers: the fine tier never carries one, and the
140
- // coarse tier writes its own below
141
- const { tags, cigarIdx } = foldCsIntoCg(rest.filter(f => !f.startsWith('cr:Z:')));
132
+ // an incoming cr:Z: or pi:i: (a PIF turned back into PAF, or a tool that
133
+ // adopted the tags) is dropped: the coarse tier writes its own fold below,
134
+ // and every row of this alignment gets this pass's row index as `pi`, the
135
+ // one id its four rows share across perspectives and tiers
136
+ const { tags, cigarIdx } = foldCsIntoCg([
137
+ ...rest.filter(f => !f.startsWith('cr:') && !f.startsWith('pi:')),
138
+ `pi:i:${stats.rows - 1}`,
139
+ ]);
142
140
  const cigar = cigarIdx === -1 ? undefined : tags[cigarIdx].slice(5);
143
141
  if (cigar !== undefined) {
144
142
  stats.cigarRows++;
@@ -161,15 +159,12 @@ function processLine(rawLine, coarseGap, stats) {
161
159
  const coarse = cigarIdx === -1 ? tags : tags.filter((_, i) => i !== cigarIdx);
162
160
  // the T row's own axis is the target, as the PAF CIGAR is written; the Q row
163
161
  // re-orients it for the query, the way the fine tier's cg is
164
- const { ops: cr, unbounded } = coarseFold({
162
+ const cr = coarseFold({
165
163
  cigar,
166
164
  coarseGap,
167
165
  ownLen: +e2 - +s2,
168
166
  mateLen: +e1 - +s1,
169
167
  });
170
- if (unbounded) {
171
- stats.unboundedRows++;
172
- }
173
168
  const tCr = cr === undefined ? [] : [`cr:Z:${cr}`];
174
169
  const qCr = cr === undefined
175
170
  ? []
@@ -215,7 +210,6 @@ export async function createPIF(filename, stream, coarseGap) {
215
210
  pairs: new Set(),
216
211
  rows: 0,
217
212
  cigarRows: 0,
218
- unboundedRows: 0,
219
213
  skipped: 0,
220
214
  };
221
215
  const transform = makePifTransform(coarseGap, stats);