← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/sequence/tracks.ts

34.9 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import { SEQUENCE_VIEWER_LIMITS, utf8ByteLength } from "../runtime-contract";
import {
  getReadCigarGeometry,
  referencesMatch,
  resolveEvidenceReferenceName,
  resolveReadReferenceRecord,
} from "./read-pileup";
import type { SequenceRecord } from "./types";

export type SequenceTrackFormat =
  "bam" | "bed" | "cram" | "gff3" | "gtf" | "sam" | "vcf";

export type SequenceTrackKind = "annotations" | "reads" | "variants";

export type TrackFeature = {
  attributes: Record<string, string>;
  codingSegments?: Array<TrackFeatureSegment>;
  end: number;
  id: string;
  label?: string;
  phase?: 0 | 1 | 2;
  reference: string;
  score?: number;
  segments?: Array<TrackFeatureSegment>;
  source?: string;
  start: number;
  strand: "+" | "-" | "." | "?";
  type: string;
};

export type TrackFeatureSegment = {
  end: number;
  start: number;
};

export type TrackVariant = {
  alternateAlleles: Array<string>;
  filters: Array<string>;
  format?: string;
  id: string;
  info: Record<string, string | true>;
  position: number;
  quality?: number;
  rawFilter?: string;
  rawId?: string;
  rawInfo?: string;
  rawQuality?: string;
  reference: string;
  referenceAllele: string;
  sampleValues?: Array<string>;
  samples: Record<string, string>;
};

export type VcfTrackHeader = {
  columns: Array<string>;
  metaLines: Array<string>;
  sampleNames: Array<string>;
};

export type TrackRead = {
  cigar: string;
  end: number;
  flags: number;
  id: string;
  insertSize: number;
  mappingQuality: number;
  matePosition?: number;
  mateReference?: string;
  position: number;
  quality?: Array<number>;
  reference: string;
  sequence: string;
  strand: "+" | "-";
  tags: Record<string, string | number>;
};

export type SequenceTrack = {
  format: SequenceTrackFormat;
  id: string;
  kind: SequenceTrackKind;
  mapping: {
    matchedReference: string | null;
    requestedReference: string | null;
    status: "matched" | "unmatched" | "unresolved";
    unmatchedReferences: Array<string>;
  };
  name: string;
  source: {
    contentHash?: string;
    displayName: string;
    workspacePath?: string;
  };
  summary: {
    itemCount: number;
    materializedItemCount?: number;
    references: Array<string>;
    truncated: boolean;
  };
  features?: Array<TrackFeature>;
  reads?: Array<TrackRead>;
  variants?: Array<TrackVariant>;
  vcfHeader?: VcfTrackHeader;
};

export type EvidenceWindow = {
  coverage: Array<{ coordinate: number; depth: number }>;
  coverageBudgetExceeded: boolean;
  coverageComplete: boolean;
  coverageOmittedReadCount: number;
  coverageReadCount: number;
  downsampled: boolean;
  end: number;
  filteredReadCount: number;
  reads: Array<TrackRead>;
  sampledReadCount: number;
  sourceReadCount: number;
  sourceReferenceUncertain: boolean;
  sourceTruncated: boolean;
  start: number;
  totalReadCount: number;
  variants: Array<TrackVariant>;
};

const MAX_TRACK_ITEMS = SEQUENCE_VIEWER_LIMITS.input.maxTrackItems;
const MAX_COVERAGE_CIGAR_CHARACTERS = 2_000_000;
const MAX_BED_BLOCKS = 10_000;
const MAX_VCF_HEADER_BYTES = SEQUENCE_VIEWER_LIMITS.session.maxSessionBytes / 2;
const MAX_VCF_HEADER_LINES = 4_096;
const MAX_VCF_SAMPLES = 10_000;

export function parseSequenceTrack({
  content,
  displayName,
  format,
  id,
  requestedReference,
  sourceContentHash,
  sourceItemCount,
  sourceTruncated,
  sourceWorkspacePath,
}: {
  content: string | Uint8Array;
  displayName: string;
  format: SequenceTrackFormat;
  id: string;
  requestedReference?: string;
  sourceContentHash?: string;
  sourceItemCount?: number;
  sourceTruncated?: boolean;
  sourceWorkspacePath?: string;
}): SequenceTrack {
  const parsed =
    format === "bam"
      ? {
          kind: "reads" as const,
          reads:
            typeof content === "string"
              ? parseSam(content)
              : parseUncompressedBam(content),
        }
      : format === "sam" || format === "cram"
        ? { kind: "reads" as const, reads: parseSam(requireText(content)) }
        : format === "vcf"
          ? {
              ...parseVcfDocument(requireText(content)),
              kind: "variants" as const,
            }
          : {
              features: parseAnnotationTrack(requireText(content), format),
              kind: "annotations" as const,
            };
  const items =
    parsed.kind === "annotations"
      ? parsed.features
      : parsed.kind === "variants"
        ? parsed.variants
        : parsed.reads;
  const references = [
    ...new Set(items.map((item) => item.reference).filter(Boolean)),
  ].sort();
  const exactReference =
    requestedReference == null
      ? null
      : resolveEvidenceReferenceName(requestedReference, references);
  return {
    ...parsed,
    format,
    id,
    mapping: {
      matchedReference: exactReference,
      requestedReference: requestedReference ?? null,
      status:
        requestedReference == null
          ? "unresolved"
          : exactReference == null
            ? "unmatched"
            : "matched",
      unmatchedReferences:
        exactReference == null ? references.slice(0, 100) : [],
    },
    name: displayName,
    source: {
      displayName,
      ...(sourceContentHash == null ? {} : { contentHash: sourceContentHash }),
      ...(sourceWorkspacePath == null
        ? {}
        : { workspacePath: sourceWorkspacePath }),
    },
    summary: {
      itemCount: Math.max(items.length, sourceItemCount ?? 0),
      materializedItemCount: items.length,
      references,
      truncated: sourceTruncated === true || items.length >= MAX_TRACK_ITEMS,
    },
  };
}

export function parseAnnotationTrack(
  content: string,
  format: "bed" | "gff3" | "gtf",
): Array<TrackFeature> {
  const features: Array<TrackFeature> = [];
  for (const [lineIndex, rawLine] of content.split(/\r\n|\n|\r/u).entries()) {
    const line = rawLine.trim();
    if (
      line.length === 0 ||
      line.startsWith("#") ||
      line.startsWith("track") ||
      line.startsWith("browser")
    )
      continue;
    const fields = rawLine.split("\t");
    if (format === "bed") {
      if (fields.length < 3)
        throw new Error(
          `BED line ${lineIndex + 1} has fewer than three columns.`,
        );
      const reference = fields[0]?.trim() ?? "";
      const zeroBasedStart = parseNonnegativeInteger(
        fields[1],
        `BED line ${lineIndex + 1} start`,
      );
      const end = parsePositiveInteger(
        fields[2],
        `BED line ${lineIndex + 1} end`,
      );
      if (end <= zeroBasedStart)
        throw new Error(
          `BED line ${lineIndex + 1} has an empty or reversed interval.`,
        );
      const label = fields[3]?.trim();
      const score = parseOptionalNumber(fields[4]);
      const segments = parseBedSegments({
        end,
        fields,
        lineNumber: lineIndex + 1,
        zeroBasedStart,
      });
      const codingSegments = parseBedCodingSegments({
        end,
        fields,
        lineNumber: lineIndex + 1,
        segments,
        zeroBasedStart,
      });
      const itemRgb = fields[8]?.trim();
      features.push({
        attributes:
          itemRgb == null || itemRgb === "." || itemRgb.length === 0
            ? {}
            : { item_rgb: parseBedItemRgb(itemRgb, lineIndex + 1) },
        ...(codingSegments.length === 0 ? {} : { codingSegments }),
        end,
        id: `${reference}:bed:${lineIndex + 1}`,
        ...(label == null || label.length === 0 ? {} : { label }),
        reference,
        ...(score == null ? {} : { score }),
        ...(fields.length < 12 ? {} : { segments }),
        start: zeroBasedStart + 1,
        strand: parseStrand(fields[5]),
        type: "region",
      });
    } else {
      if (fields.length < 9)
        throw new Error(
          `${format.toUpperCase()} line ${lineIndex + 1} has fewer than nine columns.`,
        );
      const reference = fields[0]?.trim() ?? "";
      const attributes =
        format === "gtf"
          ? parseGtfAttributes(fields[8] ?? "")
          : parseGffAttributes(fields[8] ?? "");
      const start = parsePositiveInteger(
        fields[3],
        `${format.toUpperCase()} line ${lineIndex + 1} start`,
      );
      const end = parsePositiveInteger(
        fields[4],
        `${format.toUpperCase()} line ${lineIndex + 1} end`,
      );
      if (end < start)
        throw new Error(
          `${format.toUpperCase()} line ${lineIndex + 1} has a reversed interval.`,
        );
      const label =
        attributes.Name ?? attributes.gene_name ?? attributes.transcript_name;
      const score = parseOptionalNumber(fields[5]);
      const source = fields[1]?.trim();
      const phase = parseFeaturePhase(fields[7], format, lineIndex + 1);
      features.push({
        attributes,
        end,
        id:
          attributes.ID ??
          attributes.gene_id ??
          attributes.transcript_id ??
          `${reference}:${format}:${lineIndex + 1}`,
        ...(label == null ? {} : { label }),
        ...(phase == null ? {} : { phase }),
        reference,
        ...(score == null ? {} : { score }),
        ...(source == null || source.length === 0 ? {} : { source }),
        start,
        strand: parseStrand(fields[6]),
        type: fields[2]?.trim() || "feature",
      });
    }
    if (features.length >= MAX_TRACK_ITEMS) break;
  }
  return features;
}

function parseBedSegments({
  end,
  fields,
  lineNumber,
  zeroBasedStart,
}: {
  end: number;
  fields: Array<string>;
  lineNumber: number;
  zeroBasedStart: number;
}): Array<TrackFeatureSegment> {
  if (fields.length < 10) {
    return [{ end, start: zeroBasedStart + 1 }];
  }
  if (fields.length < 12) {
    throw new Error(
      `BED line ${lineNumber} must include blockCount, blockSizes, and blockStarts together.`,
    );
  }
  const blockCount = parsePositiveInteger(
    fields[9],
    `BED line ${lineNumber} block count`,
  );
  if (blockCount > MAX_BED_BLOCKS) {
    throw new Error(`BED line ${lineNumber} exceeds the bounded block count.`);
  }
  const sizes = parseBedBlockList(fields[10], blockCount, lineNumber, "sizes");
  const starts = parseBedBlockList(
    fields[11],
    blockCount,
    lineNumber,
    "starts",
  );
  let previousEnd = zeroBasedStart;
  return starts.map((blockStart, index) => {
    const size = sizes[index] ?? 0;
    if (size === 0) {
      throw new Error(`BED line ${lineNumber} has an empty exon block.`);
    }
    const start = zeroBasedStart + blockStart;
    const blockEnd = start + size;
    if (start < previousEnd || blockEnd > end) {
      throw new Error(
        `BED line ${lineNumber} has overlapping or out-of-range exon blocks.`,
      );
    }
    previousEnd = blockEnd;
    return { end: blockEnd, start: start + 1 };
  });
}

function parseBedBlockList(
  value: string | undefined,
  expectedCount: number,
  lineNumber: number,
  label: "sizes" | "starts",
): Array<number> {
  const normalized = value?.endsWith(",") ? value.slice(0, -1) : value;
  const parts = normalized?.split(",") ?? [];
  if (
    parts.length !== expectedCount ||
    parts.some((part) => part.length === 0)
  ) {
    throw new Error(
      `BED line ${lineNumber} block ${label} do not match its block count.`,
    );
  }
  return parts.map((part, index) =>
    parseNonnegativeInteger(
      part,
      `BED line ${lineNumber} block ${label} ${index + 1}`,
    ),
  );
}

function parseBedCodingSegments({
  end,
  fields,
  lineNumber,
  segments,
  zeroBasedStart,
}: {
  end: number;
  fields: Array<string>;
  lineNumber: number;
  segments: Array<TrackFeatureSegment>;
  zeroBasedStart: number;
}): Array<TrackFeatureSegment> {
  if (fields.length < 7) return [];
  if (fields.length < 8) {
    throw new Error(
      `BED line ${lineNumber} must include both coding-boundary columns.`,
    );
  }
  const thickStart = parseNonnegativeInteger(
    fields[6],
    `BED line ${lineNumber} thickStart`,
  );
  const thickEnd = parseNonnegativeInteger(
    fields[7],
    `BED line ${lineNumber} thickEnd`,
  );
  if (thickStart < zeroBasedStart || thickEnd < thickStart || thickEnd > end) {
    throw new Error(
      `BED line ${lineNumber} has reversed or out-of-range coding boundaries.`,
    );
  }
  if (thickStart === thickEnd) return [];
  return segments.flatMap((segment) => {
    const start = Math.max(segment.start, thickStart + 1);
    const codingEnd = Math.min(segment.end, thickEnd);
    return start > codingEnd ? [] : [{ end: codingEnd, start }];
  });
}

function parseBedItemRgb(value: string, lineNumber: number): string {
  if (value === "0") return value;
  const components = value.split(",");
  if (
    components.length !== 3 ||
    components.some((component) => {
      const parsed = Number(component);
      return (
        !/^\d{1,3}$/u.test(component) ||
        !Number.isSafeInteger(parsed) ||
        parsed > 255
      );
    })
  ) {
    throw new Error(`BED line ${lineNumber} has an invalid RGB value.`);
  }
  return value;
}

function parseFeaturePhase(
  value: string | undefined,
  format: "gff3" | "gtf",
  lineNumber: number,
): 0 | 1 | 2 | undefined {
  if (value == null || value === ".") return undefined;
  if (value === "0" || value === "1" || value === "2") {
    return value === "0" ? 0 : value === "1" ? 1 : 2;
  }
  throw new Error(
    `${format.toUpperCase()} line ${lineNumber} has an invalid CDS phase.`,
  );
}

export function parseVcf(content: string): Array<TrackVariant> {
  return parseVcfDocument(content).variants;
}

function parseVcfDocument(content: string): {
  variants: Array<TrackVariant>;
  vcfHeader?: VcfTrackHeader;
} {
  let columns: Array<string> | undefined;
  let headerBytes = 0;
  const metaLines: Array<string> = [];
  const variants: Array<TrackVariant> = [];
  let sampleNames: Array<string> = [];
  for (const [lineIndex, rawLine] of content.split(/\r\n|\n|\r/u).entries()) {
    if (rawLine.trim().length === 0) continue;
    if (rawLine.startsWith("##")) {
      if (columns != null) {
        throw new Error(
          `VCF line ${lineIndex + 1} contains metadata after the column header.`,
        );
      }
      headerBytes = addVcfHeaderBytes(headerBytes, rawLine, lineIndex + 1);
      if (metaLines.length >= MAX_VCF_HEADER_LINES) {
        throw new Error("VCF metadata exceeds the bounded header line count.");
      }
      metaLines.push(rawLine);
      continue;
    }
    if (rawLine.startsWith("#CHROM")) {
      if (columns != null) {
        throw new Error(`VCF line ${lineIndex + 1} repeats the column header.`);
      }
      headerBytes = addVcfHeaderBytes(headerBytes, rawLine, lineIndex + 1);
      columns = rawLine.split("\t");
      if (columns.length < 8) {
        throw new Error(
          `VCF line ${lineIndex + 1} has fewer than eight header columns.`,
        );
      }
      sampleNames = columns.slice(9);
      if (sampleNames.length > MAX_VCF_SAMPLES) {
        throw new Error("VCF sample count exceeds the bounded track limit.");
      }
      if (new Set(sampleNames).size !== sampleNames.length) {
        throw new Error("VCF sample identifiers must be unique.");
      }
      continue;
    }
    if (rawLine.startsWith("#")) continue;
    const fields = rawLine.split("\t");
    if (fields.length < 8)
      throw new Error(
        `VCF line ${lineIndex + 1} has fewer than eight columns.`,
      );
    const reference = fields[0]?.trim() ?? "";
    const position = parsePositiveInteger(
      fields[1],
      `VCF line ${lineIndex + 1} position`,
    );
    const referenceAllele = fields[3]?.trim().toUpperCase() ?? "";
    if (referenceAllele.length === 0)
      throw new Error(`VCF line ${lineIndex + 1} has no reference allele.`);
    if (columns != null && fields.length !== columns.length) {
      throw new Error(
        `VCF line ${lineIndex + 1} has ${fields.length} columns; its header declares ${columns.length}.`,
      );
    }
    const quality = parseOptionalNumber(fields[5]);
    const format = fields[8];
    const sampleValues = fields.slice(9);
    variants.push({
      alternateAlleles: (fields[4] ?? "")
        .split(",")
        .filter((value) => value.length > 0 && value !== "."),
      filters:
        fields[6] === "." || fields[6] === "PASS"
          ? []
          : (fields[6] ?? "").split(";"),
      ...(format == null ? {} : { format }),
      id:
        fields[2] === "." || fields[2] == null
          ? `${reference}:${position}:${referenceAllele}`
          : fields[2],
      info: parseVcfInfo(fields[7] ?? ""),
      position,
      ...(quality == null ? {} : { quality }),
      rawFilter: fields[6] ?? ".",
      rawId: fields[2] ?? ".",
      rawInfo: fields[7] ?? ".",
      rawQuality: fields[5] ?? ".",
      reference,
      referenceAllele,
      ...(sampleNames.length === 0 ? {} : { sampleValues }),
      samples: Object.fromEntries(
        sampleNames.map((name, index) => [name, sampleValues[index] ?? "."]),
      ),
    });
    if (variants.length >= MAX_TRACK_ITEMS) break;
  }
  return {
    variants,
    ...(columns == null
      ? {}
      : { vcfHeader: { columns, metaLines, sampleNames } }),
  };
}

function addVcfHeaderBytes(
  currentBytes: number,
  line: string,
  lineNumber: number,
): number {
  const nextBytes = currentBytes + utf8ByteLength(line) + 1;
  if (nextBytes > MAX_VCF_HEADER_BYTES) {
    throw new Error(
      `VCF line ${lineNumber} exceeds the bounded metadata header size.`,
    );
  }
  return nextBytes;
}

export function parseSam(content: string): Array<TrackRead> {
  const reads: Array<TrackRead> = [];
  for (const [lineIndex, rawLine] of content.split(/\r\n|\n|\r/u).entries()) {
    if (rawLine.startsWith("@") || rawLine.trim().length === 0) continue;
    const fields = rawLine.split("\t");
    if (fields.length < 11)
      throw new Error(
        `SAM line ${lineIndex + 1} has fewer than eleven columns.`,
      );
    const flags = parseNonnegativeInteger(
      fields[1],
      `SAM line ${lineIndex + 1} flags`,
    );
    if (flags > 0xffff) {
      throw new Error(
        `SAM line ${lineIndex + 1} flags exceed the 16-bit SAM flag field.`,
      );
    }
    const reference = fields[2] ?? "*";
    if ((flags & 0x4) !== 0 || reference === "*") continue;
    const position = parsePositiveInteger(
      fields[3],
      `SAM line ${lineIndex + 1} position`,
    );
    const cigar = fields[5] ?? "*";
    validateCigar(cigar, lineIndex + 1);
    const sequence = fields[9] === "*" ? "" : (fields[9]?.toUpperCase() ?? "");
    const qualityString = fields[10] ?? "*";
    if (qualityString !== "*" && qualityString.length !== sequence.length) {
      throw new Error(
        `SAM line ${lineIndex + 1} has ${sequence.length} sequence symbols and ${qualityString.length} quality symbols.`,
      );
    }
    if (
      qualityString !== "*" &&
      [...qualityString].some(
        (symbol) => symbol.charCodeAt(0) < 33 || symbol.charCodeAt(0) > 126,
      )
    ) {
      throw new Error(
        `SAM line ${lineIndex + 1} quality must use printable Phred+33 ASCII.`,
      );
    }
    const mappingQuality = parseNonnegativeInteger(
      fields[4],
      `SAM line ${lineIndex + 1} mapping quality`,
    );
    if (mappingQuality > 255) {
      throw new Error(`SAM line ${lineIndex + 1} mapping quality exceeds 255.`);
    }
    const matePosition = parseOptionalPositiveInteger(fields[7]);
    const mateReference =
      fields[6] === "=" ? reference : fields[6] === "*" ? undefined : fields[6];
    reads.push({
      cigar,
      end: position + referenceSpan(cigar, sequence.length) - 1,
      flags,
      id: fields[0] ?? `read-${lineIndex + 1}`,
      insertSize: parseInteger(
        fields[8],
        `SAM line ${lineIndex + 1} insert size`,
      ),
      mappingQuality,
      ...(matePosition == null ? {} : { matePosition }),
      ...(mateReference == null ? {} : { mateReference }),
      position,
      ...(qualityString === "*"
        ? {}
        : {
            quality: [...qualityString].map((symbol) =>
              Math.max(0, symbol.charCodeAt(0) - 33),
            ),
          }),
      reference,
      sequence,
      strand: (flags & 0x10) !== 0 ? "-" : "+",
      tags: parseSamTags(fields.slice(11)),
    });
    if (reads.length >= MAX_TRACK_ITEMS) break;
  }
  return reads;
}

export function parseUncompressedBam(bytes: Uint8Array): Array<TrackRead> {
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  if (
    bytes.length < 12 ||
    String.fromCharCode(...bytes.slice(0, 4)) !== "BAM\u0001"
  )
    throw new Error(
      "BAM data must be BGZF-decompressed and begin with BAM\\1.",
    );
  let offset = 4;
  const headerLength = view.getInt32(offset, true);
  offset += 4 + headerLength;
  if (offset + 4 > bytes.length) throw new Error("BAM header is truncated.");
  const referenceCount = view.getInt32(offset, true);
  offset += 4;
  const references: Array<string> = [];
  for (let index = 0; index < referenceCount; index += 1) {
    if (offset + 8 > bytes.length)
      throw new Error("BAM reference dictionary is truncated.");
    const nameLength = view.getInt32(offset, true);
    offset += 4;
    const name = decodeAscii(
      bytes.slice(offset, offset + Math.max(0, nameLength - 1)),
    );
    offset += nameLength + 4;
    references.push(name);
  }
  const reads: Array<TrackRead> = [];
  while (offset + 4 <= bytes.length && reads.length < MAX_TRACK_ITEMS) {
    const blockSize = view.getInt32(offset, true);
    offset += 4;
    if (blockSize < 32 || offset + blockSize > bytes.length)
      throw new Error("BAM alignment block is truncated or invalid.");
    const blockEnd = offset + blockSize;
    const referenceId = view.getInt32(offset, true);
    const zeroBasedPosition = view.getInt32(offset + 4, true);
    const binMqNl = view.getUint32(offset + 8, true);
    const flagNc = view.getUint32(offset + 12, true);
    const sequenceLength = view.getInt32(offset + 16, true);
    const nextReferenceId = view.getInt32(offset + 20, true);
    const nextPosition = view.getInt32(offset + 24, true);
    const insertSize = view.getInt32(offset + 28, true);
    const readNameLength = binMqNl & 0xff;
    const mappingQuality = (binMqNl >>> 8) & 0xff;
    const cigarCount = flagNc & 0xffff;
    const flags = flagNc >>> 16;
    let cursor = offset + 32;
    const id = decodeAscii(
      bytes.slice(cursor, cursor + Math.max(0, readNameLength - 1)),
    );
    cursor += readNameLength;
    const cigarParts: Array<string> = [];
    for (let index = 0; index < cigarCount; index += 1) {
      const encoded = view.getUint32(cursor, true);
      cursor += 4;
      cigarParts.push(`${encoded >>> 4}${"MIDNSHP=XB"[encoded & 0xf] ?? "?"}`);
    }
    const sequenceByteCount = Math.ceil(sequenceLength / 2);
    const sequence = decodeBamSequence(
      bytes.slice(cursor, cursor + sequenceByteCount),
      sequenceLength,
    );
    cursor += sequenceByteCount;
    const rawQuality = [...bytes.slice(cursor, cursor + sequenceLength)];
    cursor += sequenceLength;
    const reference = references[referenceId] ?? `reference-${referenceId}`;
    const cigar = cigarParts.join("") || "*";
    if ((flags & 0x4) === 0 && referenceId >= 0) {
      const position = zeroBasedPosition + 1;
      reads.push({
        cigar,
        end: position + referenceSpan(cigar, sequence.length) - 1,
        flags,
        id,
        insertSize,
        mappingQuality,
        ...(nextPosition < 0 ? {} : { matePosition: nextPosition + 1 }),
        ...(nextReferenceId < 0
          ? {}
          : {
              mateReference:
                references[nextReferenceId] ?? `reference-${nextReferenceId}`,
            }),
        position,
        ...(rawQuality.every((quality) => quality === 0xff)
          ? {}
          : { quality: rawQuality }),
        reference,
        sequence,
        strand: (flags & 0x10) !== 0 ? "-" : "+",
        tags: parseBamAux(bytes.slice(cursor, blockEnd)),
      });
    }
    offset = blockEnd;
  }
  return reads;
}

export function buildEvidenceWindow({
  end,
  maxReads = 100,
  readFilter,
  reference,
  referenceRecords,
  start,
  tracks,
}: {
  end: number;
  maxReads?: number;
  readFilter?: (read: TrackRead) => boolean;
  reference: string;
  referenceRecords?: ReadonlyArray<Pick<SequenceRecord, "sourceLabel">>;
  start: number;
  tracks: Array<SequenceTrack>;
}): EvidenceWindow {
  if (!Number.isSafeInteger(start) || start < 1 || !Number.isSafeInteger(end))
    throw new Error(
      "Evidence window coordinates must be positive safe integers.",
    );
  if (end < start)
    throw new Error("Evidence window end must be at or after its start.");
  if (!Number.isSafeInteger(maxReads) || maxReads < 0)
    throw new Error("Evidence read limit must be a nonnegative safe integer.");
  const span = end - start + 1;
  if (span > 100_000)
    throw new Error(
      "Read evidence is available only for windows of 100,000 bases or fewer.",
    );
  const selectedReferenceRecord =
    referenceRecords == null
      ? null
      : resolveReadReferenceRecord(referenceRecords, reference);
  const trackReferences = tracks.map((track) => {
    const candidate = resolveEvidenceReferenceName(
      reference,
      track.summary.references,
    );
    const sourceRecord =
      referenceRecords == null || candidate == null
        ? null
        : resolveReadReferenceRecord(referenceRecords, candidate);
    const ambiguousDocumentReference =
      referenceRecords != null &&
      referenceRecords.some((record) =>
        referencesMatch(record.sourceLabel, reference),
      ) &&
      selectedReferenceRecord == null;
    const belongsToAnotherRecord =
      selectedReferenceRecord != null &&
      sourceRecord?.sourceLabel !== selectedReferenceRecord.sourceLabel;
    return {
      track,
      matchedReference:
        ambiguousDocumentReference || belongsToAnotherRecord ? null : candidate,
    };
  });
  const allReads = trackReferences
    .flatMap(({ track, matchedReference }) =>
      (track.reads ?? []).filter((read) => read.reference === matchedReference),
    )
    .filter(
      (read) =>
        (read.flags & 0x4) === 0 && read.end >= start && read.position <= end,
    );
  const filteredReads =
    readFilter == null ? allReads : allReads.filter(readFilter);
  const matchingReadTracks = trackReferences.filter(
    ({ track, matchedReference }) =>
      track.kind === "reads" &&
      (track.summary.truncated ||
        matchedReference != null ||
        track.summary.references.some((name) =>
          referencesMatch(name, reference),
        )),
  );
  const sourceReadCount = matchingReadTracks.reduce(
    (sum, { track }) => sum + track.summary.itemCount,
    0,
  );
  const sourceTruncated = matchingReadTracks.some(
    ({ track }) => track.summary.truncated,
  );
  // A missing name in a truncated sample (or an ambiguous alias) is not proof
  // that the reference is absent from the source, even when the observed depth is zero.
  const sourceReferenceUncertain = matchingReadTracks.some(
    ({ matchedReference }) => matchedReference == null,
  );
  const variants = trackReferences
    .flatMap(({ track, matchedReference }) =>
      (track.variants ?? []).filter(
        (variant) => variant.reference === matchedReference,
      ),
    )
    .filter((variant) => variant.position >= start && variant.position <= end);
  const coverage = new Int32Array(span + 1);
  let coverageReadCount = 0;
  let coverageCigarCharacters = 0;
  let coverageBudgetExceeded = false;
  for (const read of filteredReads) {
    coverageCigarCharacters += read.cigar.length;
    if (coverageCigarCharacters > MAX_COVERAGE_CIGAR_CHARACTERS) {
      coverageBudgetExceeded = true;
      continue;
    }
    try {
      const { intervals } = getReadCigarGeometry(read, { end, start });
      // Only M, = and X contribute sequenced-base coverage. D and N consume
      // reference coordinates but are not covered bases; I/S/H/P consume none.
      for (const interval of intervals) {
        coverage[interval.start1 - start] += 1;
        coverage[interval.end1 - start + 1] -= 1;
      }
      coverageReadCount += 1;
    } catch {
      // Missing, malformed or unsupported CIGARs must not become fabricated
      // POS..END coverage. Callers disclose this explicit incompleteness.
    }
  }
  let depth = 0;
  const coveragePoints = Array.from({ length: span }, (_, index) => {
    depth += coverage[index] ?? 0;
    return { coordinate: start + index, depth };
  });
  const reads = downsampleReads(filteredReads, maxReads);
  const coverageOmittedReadCount = filteredReads.length - coverageReadCount;
  return {
    coverage: coveragePoints,
    coverageBudgetExceeded,
    coverageComplete:
      !sourceTruncated &&
      !sourceReferenceUncertain &&
      coverageOmittedReadCount === 0,
    coverageOmittedReadCount,
    coverageReadCount,
    downsampled: sourceTruncated || reads.length < filteredReads.length,
    end,
    filteredReadCount: filteredReads.length,
    reads,
    sampledReadCount: reads.length,
    sourceReadCount,
    sourceReferenceUncertain,
    sourceTruncated,
    start,
    totalReadCount: allReads.length,
    variants,
  };
}

function downsampleReads(
  reads: Array<TrackRead>,
  maxReads: number,
): Array<TrackRead> {
  if (reads.length <= maxReads) return reads;
  const step = reads.length / maxReads;
  return Array.from(
    { length: maxReads },
    (_, index) => reads[Math.floor(index * step)],
  ).filter((read): read is TrackRead => read != null);
}

function parseGffAttributes(value: string): Record<string, string> {
  return Object.fromEntries(
    value.split(";").flatMap((entry) => {
      const [rawKey, ...rawValue] = entry.split("=");
      const key = rawKey?.trim();
      return key == null || key.length === 0
        ? []
        : [
            [
              decodeURIComponent(key),
              decodeURIComponent(rawValue.join("=").trim()),
            ],
          ];
    }),
  );
}

function parseGtfAttributes(value: string): Record<string, string> {
  const attributes: Record<string, string> = {};
  for (const match of value.matchAll(/([^\s;]+)\s+"([^"]*)"\s*;?/gu)) {
    if (match[1] != null) attributes[match[1]] = match[2] ?? "";
  }
  return attributes;
}

function parseVcfInfo(value: string): Record<string, string | true> {
  if (value === "." || value.length === 0) return {};
  return Object.fromEntries(
    value.split(";").map((entry) => {
      const [key, ...rest] = entry.split("=");
      return [key ?? "", rest.length === 0 ? true : rest.join("=")];
    }),
  );
}

function parseSamTags(values: Array<string>): Record<string, string | number> {
  const tags: Record<string, string | number> = {};
  for (const value of values) {
    const [tag, type, ...rawValue] = value.split(":");
    if (tag == null || type == null || !/^[A-Za-z][A-Za-z0-9]$/u.test(tag)) {
      throw new Error(`SAM optional field '${value}' has an invalid tag.`);
    }
    const joined = rawValue.join(":");
    if (!/^[AifZHB]$/u.test(type)) {
      throw new Error(
        `SAM optional field '${tag}' has unsupported type '${type}'.`,
      );
    }
    if (type === "i" || type === "f") {
      const parsed = Number(joined);
      if (
        !Number.isFinite(parsed) ||
        (type === "i" && !Number.isInteger(parsed))
      ) {
        throw new Error(
          `SAM optional field '${tag}' has an invalid numeric value.`,
        );
      }
      tags[tag] = parsed;
      continue;
    }
    tags[tag] = joined;
  }
  return tags;
}

function parseBamAux(bytes: Uint8Array): Record<string, string | number> {
  const tags: Record<string, string | number> = {};
  let offset = 0;
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  while (offset + 3 <= bytes.length) {
    const tag = String.fromCharCode(bytes[offset] ?? 0, bytes[offset + 1] ?? 0);
    const type = String.fromCharCode(bytes[offset + 2] ?? 0);
    offset += 3;
    if (type === "A" || type === "c" || type === "C") {
      tags[tag] =
        type === "A"
          ? String.fromCharCode(bytes[offset] ?? 0)
          : type === "c"
            ? view.getInt8(offset)
            : view.getUint8(offset);
      offset += 1;
    } else if (type === "s" || type === "S") {
      tags[tag] =
        type === "s"
          ? view.getInt16(offset, true)
          : view.getUint16(offset, true);
      offset += 2;
    } else if (type === "i" || type === "I" || type === "f") {
      tags[tag] =
        type === "i"
          ? view.getInt32(offset, true)
          : type === "I"
            ? view.getUint32(offset, true)
            : view.getFloat32(offset, true);
      offset += 4;
    } else if (type === "Z" || type === "H") {
      const end = bytes.indexOf(0, offset);
      if (end < 0) break;
      tags[tag] = decodeAscii(bytes.slice(offset, end));
      offset = end + 1;
    } else break;
  }
  return tags;
}

function decodeBamSequence(bytes: Uint8Array, length: number): string {
  const alphabet = "=ACMGRSVTWYHKDBN";
  let sequence = "";
  for (let index = 0; index < length; index += 1) {
    const encoded = bytes[Math.floor(index / 2)] ?? 0;
    const code = index % 2 === 0 ? encoded >>> 4 : encoded & 0xf;
    sequence += alphabet[code] ?? "N";
  }
  return sequence;
}

function referenceSpan(cigar: string, fallbackLength: number): number {
  if (cigar === "*" || cigar.length === 0) return Math.max(1, fallbackLength);
  let span = 0;
  for (const match of cigar.matchAll(/(\d+)([MIDNSHP=XB])/gu)) {
    if (match[2] != null && "MDN=X".includes(match[2]))
      span += Number(match[1] ?? 0);
  }
  return Math.max(1, span);
}

function validateCigar(cigar: string, lineNumber: number): void {
  if (cigar === "*") return;
  if (!/^(?:[1-9]\d*[MIDNSHP=XB])+$/u.test(cigar)) {
    throw new Error(`SAM line ${lineNumber} has an invalid CIGAR string.`);
  }
}

function parseStrand(value: string | undefined): TrackFeature["strand"] {
  return value === "+" || value === "-" || value === "." || value === "?"
    ? value
    : ".";
}

function parsePositiveInteger(
  value: string | undefined,
  label: string,
): number {
  const parsed = Number(value);
  if (!Number.isSafeInteger(parsed) || parsed <= 0)
    throw new Error(`${label} must be a positive integer.`);
  return parsed;
}

function parseNonnegativeInteger(
  value: string | undefined,
  label: string,
): number {
  const parsed = Number(value);
  if (!Number.isSafeInteger(parsed) || parsed < 0)
    throw new Error(`${label} must be a nonnegative integer.`);
  return parsed;
}

function parseInteger(value: string | undefined, label: string): number {
  const parsed = Number(value);
  if (!Number.isSafeInteger(parsed))
    throw new Error(`${label} must be an integer.`);
  return parsed;
}

function parseOptionalPositiveInteger(
  value: string | undefined,
): number | undefined {
  const parsed = Number(value);
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
}

function parseOptionalNumber(value: string | undefined): number | undefined {
  const parsed = Number(value);
  return value == null || value === "." || !Number.isFinite(parsed)
    ? undefined
    : parsed;
}

function decodeAscii(bytes: Uint8Array): string {
  return new TextDecoder("ascii").decode(bytes);
}

function requireText(content: string | Uint8Array): string {
  if (typeof content !== "string")
    throw new Error("This track format requires UTF-8 text input.");
  return content;
}

function requireBytes(content: string | Uint8Array): Uint8Array {
  if (typeof content === "string")
    throw new Error("BAM tracks require decompressed binary input.");
  return content;
}

SHA-256: 2714f6a9d1d98036c9ff9bfdebc534c4a4502d6828ccb1ccc64d50c431c0f2a5