Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 2x 2x 2x 2x 16x 6x 6x 16x 16x 1x 1x 16x 2x 36x 36x 2x 31x 31x 2x 2x 2x 2x 151x 151x 145x 145x 141x 141x 141x 2x 41x 41x 38x 41x 41x 41x 41x 36x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 1x 35x 41x 41x 41x 34x 34x 34x 34x 34x 34x 34x 33x 34x 2x 2x 34x | /**
* The notebooks published beside the app, as *About* names them (SPECS §18.10).
*
* Five Quarto notebooks, each published at `<site>/<path>/` with a
* `rendered.json` beside it: when it was rendered, from which build, by which
* run, on what machine, and the one line of what it found
* (`notebooks/scripts/rendered.py`). `.github/workflows/notebooks.yml` renders
* and publishes the validation, the timings, the viewers and the parity with
* every release, each on a runner of its own.
* The laptop is rendered by hand, on a laptop, and committed as
* `notebooks/laptop.html`, which is what the site publishes; the workflow's
* own render of it is a run's artifact and nothing more. This module is what
* the app knows of that: which pages there are, what each is for, where the
* site holding them is, and how to read a stamp without believing it.
*
* Where each page is published is `notebooks/scripts/pages.py`'s table, and
* `test/unit/notebooks.test.ts` holds this list to it and to
* `NOTEBOOK_SEGMENTS`, so a sixth notebook cannot be published without *About*
* naming it.
*
* The stamp is read on demand, when the reader opens the tab that shows it,
* and never at startup — the reasoning `versions.ts` gives for its manifest. It
* is untrusted input from the network that ends up as text and a link on
* screen, so a field that fails its check is dropped rather than repaired.
*/
import { HOMEPAGE, TARGET } from './version'
import { IS_PORTABLE, NOTEBOOK_SEGMENTS, SITE_BASE } from './versions'
/** One published notebook. */
export interface Notebook {
/** Where it is published under the site, without its slash. */
path: (typeof NOTEBOOK_SEGMENTS)[number]
/** As its page is titled. */
title: string
/** What it holds chromosomix to, in a sentence. */
what: string
/**
* Rendered by hand and committed rather than by the workflow: `local` in
* `pages.py`'s table.
*/
byHand?: true
}
/** In the order the workflow lists them, and `NOTEBOOK_SEGMENTS` with it. */
export const NOTEBOOKS: readonly Notebook[] = [
{
path: 'validation',
title: 'Validation',
what:
'Every analysis run twice over public data, once by chromosomix and once by the package it ' +
'reproduces — cooltools, coolpuppy, chromosight, deepTools, bedtools and the rest — and the two ' +
'lined up bin by bin or call by call.',
},
{
path: 'timings',
title: 'Timings',
what:
'Every command of the validation run again, as a fresh process, four times round robin, and its ' +
'CPU time, wall-clock time and peak memory set beside its reference’s.',
},
{
path: 'viewers',
title: 'Viewers',
what:
'The time to a picture beside juicebox.js and HiGlass: one map and one track, the same files in ' +
'each viewer, from disk and from the bucket they are published in.',
},
{
path: 'parity',
title: 'Parity',
what:
'Ten tasks run in the app, in headless Chromium, Firefox and WebKit, then again from the line its ' +
'console wrote in each, and every file the two wrote compared.',
},
{
path: 'laptop',
title: 'Laptop',
what:
'What a genome-wide analysis costs on a laptop: 23 of them run on the command line and in the ' +
'app, with their time and peak memory, then six over a deep human map, start to finish, against ' +
'cooltools and coolpuppy. Render it on your own to measure yours.',
byHand: true,
},
]
/**
* The site the notebooks are published on, with its trailing slash, or `null`
* when this build can name none.
*
* **This page's own site when it is one**: the published app, an archived pin
* or a preview, which all reach up to `SITE_BASE`, where the workflow put the
* pages beside the app. **The deployed site, by its homepage, otherwise**: the
* desktop and the single file have no site of their own, the package's copy is
* unpacked into somebody else's, and a dev server serves the app and nothing
* the workflow wrote — the rule `ui/stores/registry.ts` follows for the
* stores, with the dev server added, since the stores are in the checkout and
* the notebooks are not.
*/
export function notebooksSite(
where: {
target: typeof TARGET
portable: boolean
dev: boolean
href: string | null
siteBase: string
homepage: string
} = {
target: TARGET,
portable: IS_PORTABLE,
dev: import.meta.env.DEV,
href: typeof window === 'undefined' ? null : window.location.href,
siteBase: SITE_BASE,
homepage: HOMEPAGE,
},
): string | null {
const onSite = where.target === 'web' && !where.portable && !where.dev
if (onSite && where.href !== null && /^https?:/.test(where.href)) {
try {
return new URL(where.siteBase, where.href).href
} catch {
// A base that does not resolve is not a site this page is on.
}
}
if (where.homepage === '') return null
try {
const home = new URL(where.homepage).href
return home.endsWith('/') ? home : `${home}/`
} catch {
return null
}
}
/** Where a notebook's page is, on `site`. */
export function notebookUrl(site: string, notebook: Notebook): string {
return new URL(`${notebook.path}/`, site).href
}
/** Where its `rendered.json` is, on `site`. */
export function stampUrl(site: string, notebook: Notebook): string {
return new URL(`${notebook.path}/rendered.json`, site).href
}
/** What a `rendered.json` says, once it has survived being read. */
export interface NotebookStamp {
/** When the render finished, ISO 8601. */
finished: string
/** How long it took, in seconds, or `null`. */
seconds: number | null
/** The `package.json` version it was rendered from. */
version: string
/** The commit, short, or `''`. */
commit: string
/** The workflow run's page, and its number, or `null` for a render by hand. */
run: { url: string; number: string } | null
/** The runner, as cores and gigabytes, or `null`. */
machine: { cores: number; memoryGb: number | null } | null
/** The one line of what the page found, or `''`. */
summary: string
}
/** Longer than any line the five notebooks write, and short enough for a dialog. */
const SUMMARY_LIMIT = 600
const VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]{1,40})?$/
const COMMIT = /^[0-9a-f]{7,40}$/
/** A run of this repository's, or of a fork's, on GitHub: the one host the workflow runs on. */
const RUN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/actions\/runs\/\d+$/
function record(value: unknown): Record<string, unknown> | null {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
return value as Record<string, unknown>
}
function text(value: unknown, pattern: RegExp): string {
return typeof value === 'string' && pattern.test(value) ? value : ''
}
/**
* A `rendered.json`, checked field by field.
*
* `null` when it has no finishing time or no version, which are what make it a
* stamp at all; any other field that fails its check is left out.
*/
export function parseStamp(raw: unknown): NotebookStamp | null {
const stamp = record(raw)
if (stamp === null) return null
const finished =
typeof stamp.finished === 'string' && !Number.isNaN(Date.parse(stamp.finished)) ? stamp.finished : ''
const build = record(stamp.chromosomix)
const version = text(build?.version, VERSION)
if (finished === '' || version === '') return null
const seconds =
typeof stamp.seconds === 'number' && Number.isFinite(stamp.seconds) && stamp.seconds >= 0 ? stamp.seconds : null
const run = record(stamp.run)
const runUrl = text(run?.url, RUN)
const machine = record(stamp.machine)
const cores =
typeof machine?.cores === 'number' && Number.isInteger(machine.cores) && machine.cores > 0 ? machine.cores : null
const memory = machine?.memory_gb
const summary = typeof stamp.summary === 'string' ? stamp.summary.replace(/\s+/g, ' ').trim() : ''
return {
finished,
seconds,
version,
commit: text(build?.commit, COMMIT).slice(0, 7),
run: runUrl === '' ? null : { url: runUrl, number: text(run?.number, /^\d{1,9}$/) },
machine:
cores === null
? null
: { cores, memoryGb: typeof memory === 'number' && Number.isFinite(memory) && memory > 0 ? memory : null },
summary: summary.length > SUMMARY_LIMIT ? '' : summary,
}
}
/** How a stamp is fetched: `fetch`, or a stand-in for it. */
export type StampReader = (url: string) => Promise<Response>
/**
* Read a notebook's stamp: the parsed stamp, or `null` for any failure — the
* network, a page not published yet, a file that does not parse.
*
* A failure is not reported: the link to the page is still there, and a reader
* offline learns nothing from being told so in a dialog about something else.
*/
export async function fetchStamp(
url: string,
read: StampReader = (input) => fetch(input),
): Promise<NotebookStamp | null> {
try {
const response = await read(url)
if (!response.ok) return null
return parseStamp(await response.json())
} catch {
return null
}
}
|