+-- boxes otherwise), and a verification (attestation, certificate chain, bundle, capsule, ledger, identity card) opens with VERIFIED or NOT VERIFIED and how many of its checks passed. format() still returns the plain lines.stkflow(), staysmry(), with Lakner's steady-state caution), the justice, Indigenous, Truth and Reconciliation, census, search, pollution and air-dispersion results, and the signing, provenance and archive objects. 49 classes that printed as a raw list now have a method; a signing key prints its scheme, fingerprint and public key, never its secret.ldgrchk(), vrfmrg(), orchk(), vrfrcdmnf(), xfrvrf(), fetchvrf(), staysmry(), plrcf(), exprspplr(), piperate(), cmlrisk(), bufasgn(), trndtst() and stepchg() return their result with a class put in front of its existing ones, so it prints as a box; no element or value changed, and a data frame stays a data frame.adp(), alos(), admits(), adpcnts() and perdays() print as a box too: the number, the inputs it came from, the formula (adp = days / t, alos = days / n, admits = adp x t / alos), a plain reading and Lakner's caveats (steady state; a short period biases the average stay downward). The value is unchanged, with class rmbl_<name> and rmbl_measure put in front; arithmetic, round(), sum(), mean(), [, c() and as.numeric() give a plain number, and the measures still feed each other (adp(days, t = perdays(...))).level, parid, rootid), sample and non-response columns of cnsprof() and the gender, age, DGUID and quality columns of profshr(); every row of cnscwb() (up to 100 rows for the census tables, where 8 or 16 were shown); the pooled row's mark in indgap() and ordcmp(); which proxies are shared in mhcont(); a domain in welprof(); logpvalue in mahaoutl(); the trend statistics of dispmix() to two decimals; the key usage of certprs(); the whole check names of ldgrchk() and a WARN mark for a check that only warns; and, in cpschk(), each capsule's source, pinned SHA-256, size, rows and bounds, every check row with its detail, whole digests, the footprint line and the package and R versions.ginicoef(), attrfrac(), drftks(), drftpsi(), drftchisq() and drfthomog() print as a box like the Lakner measures: the number (or a small table of the named statistics), the inputs it came from, what the scale means, a reading and the caveats. The values are unchanged, with class rmbl_<name> and rmbl_measure put in front; arithmetic, [, c() and as.numeric() give plain numbers. Code that compares one of these results with identical() against a bare number now needs as.numeric() first (identical(as.numeric(ginicoef(x)), g)), because the class is kept on the value. The arithmetic primitives (cmn(), csdev() and the rest) stay plain numbers.mkmnf(), capenv(), loadprov() and citecps() return their list with a class put in front (rmbl_manifest, rmbl_capenv, rmbl_provenance, rmbl_citation), so they print as a box; every element is unchanged. dgstobj() of these objects differs from 1.0.2 because they now carry a class; signatures (mnfdgst(), mnfcanon()) and the JSON written to disk (tojs(), wrmnfjs()) are byte for byte unchanged.flag column is unchanged.ralloc() makes the randomisation list of a trial: simple, permuted-block (block sizes drawn at random) or stratified, in any ratio, with kit codes for blinded dispensing, reproducible from a seed through the package's NIST SP 800-90A generator and exactly unbiased (rejection sampling, Fisher-Yates). It returns one SHA-256 commitment to the list and the seed, to sign and timestamp before the first participant; its box shows the counts per arm, never who is in which arm, nor the seed. At unblinding rallocvrf() shows that the list used is the one committed to and that the seed and settings produce it row for row. vignette("provenance") walks a trial from the declared outcomes to the check at unblinding.mkset() boxes results from base R and other packages too: models (lm, glm, nls, survival's coxph and survreg, lme4, nlme's lme, mgcv's gam, fixest, MASS's polr and glm.nb, nnet's multinom, survey's svyglm) with their estimates, 95% intervals, hazard, odds or rate ratios where the link gives one, and a reading of which terms differ from zero; tests (t.test(), chisq.test(), cor.test() and the rest), analysis-of-variance tables, TukeyHSD(), prcomp(), kmeans(), survival curves, tables and data frames each in a box of their own; anything else as its own printout inside a box. Their own print() is unchanged.mkset() puts several results into one report: printed, a box of contents and then each result's box at one width; written, one self-contained HTML page (each box with its plot), Markdown for a GitHub comment or a paper, or plain text, the file's extension picking which. A name gives a result its heading (mkset("Change by year" = anltab(...), "Rates" = rtcalc(...))).drpconst(), drpempty(), duprows() and clncolnms() gain quiet = FALSE: unless quiet, they show one boxed summary of what they did (rows and columns before and after, the columns dropped or renamed, how many rows are duplicates) through message(), so suppressMessages() silences it too. What they return is unchanged.srchpod() takes the sweep width, track length and area (W, effort, area) in place of coverage, one area or one per cell, with area also a probability map from datumpmap() whose cell areas it uses. srchscs() takes a map and an opteffort() allocation (or PODs laid out like the map) and returns the map after a search that found nothing as map, ready for the next opteffort() and plot(). The help page and vignette("search-and-rescue") ("Your own search, step by step") walk a search from the last known position to the next day's plan, one call a step. Defaults are unchanged.evstudy() takes weights (w), loounit() passes weights to each fit (w), mtchdid() clusters its standard errors (cl), and tse() takes the confidence level of its robust interval (conflvl; it was fixed at 95%). Every estimator's help page and vignette("policy-evaluation") ("Your own panel, step by step") show a panel typed in the example: unit, time, outcome, treatment, covariates, clusters and weights.trchlthgap() takes the label of the all-ages row (agetot) and uses a table of your own without an age column whole; its help page named two of indiex()'s tables wrongly. The Indigenous estimators' help pages show a table of your own, and vignette("first-nations-metis-inuit-health") ("Your own data, step by step") goes from a published table, from microdata through svydes() and svyest(), and from plain numbers to the estimators.tse: x must come from evstudy()).mkbndl() builds a reproducibility bundle from any project folder: one zip a reviewer unzips and double-clicks (START_HERE.command on macOS, START_HERE.bat on Windows, start_here.sh on Linux). The runner checks the packages the project lists, finds the data file, takes it from --data, uses the copy inside the bundle or downloads it and checks it against the pinned SHA-256 (or runs on synthetic data from the project's recipe), runs the analysis and summarises its manifest.json. It is written in R: no zip program, Python or shell is needed, so bundles build on Windows too, and the launchers keep their executable bit inside the zip. Before zipping it checks that every internal function the bundled files call is in the bundle, and refuses a bundle that would stop with "could not find function" on the reviewer's machine.rmbl pack DIR [--dest D] [--data FILE] builds the zip and rmbl pack --new DIR starts a project. vignette("provenance") walks through one, from the starter project to what the reviewer sees.rmbl functions PATTERN (and blcli(c("functions", PATTERN))) matches the help page's title as well as the name, ignoring case in the title, so rmbl functions bundle finds mkbndl().arcpush() (and rmbl archive push DIR) sends a capture kept on this computer by arccap() to an archive.org item, archive.rmorie.com or both later, when they answer again: the same files, each checked first against the SHA-256 in the capture's manifest and never downloaded again, so the archive keeps what was captured. It records what each archive took in push.json, so running it again retries only what failed. With upload = "wayback" it asks the Wayback Machine to capture the addresses (the Wayback Machine takes no uploads, so its copy is of the page as it is then), and it timestamps a manifest that has no timestamp yet.mkbndlchk() (and rmbl pack --check ZIP) checks a built bundle before it is shipped: it unzips it into a new folder, runs it as a recipient would (setup_and_run.R --quick, or --synthetic), reads the manifest the run writes and fails when the run stops, a cross-check is DIFFER or, on real data, the counts differ from expected in config.json; a launcher the zip does not mark executable is a warning. It replaces the repository's scripts/verify_bundle.sh, which was not in the package and needed Python.mkbndlproj() writes a starter project (config.json, data_provenance.json, analysis.R) that builds and runs as it is, on synthetic or real data, for a project of your own.mkbndl("otis-mrp", ...)), the launchers and the page templates now ship with the package (inst/bundle), so an installed package builds bundles; before they were only in the source repository, and make_bundle.sh (now removed: mkbndl(), or rmbl pack from a shell, does what it did) needed bash, Python, zip and shasum. The runner's OTIS-only steps (the DML recompute, the region map shapefile, OTIS_MRP_SYNTHETIC) are now settings of that project's config.json (optional_stages, env_packages, synthetic_env), so other projects get none of them.yoy() reads text periods in time order whatever the years: fiscal years ("2019/20", "2019-2020", "FY 2019-20"; "2020/21" no longer comes before "2019/20"), one-year fiscal labels ("FY2020"), quarters ("2021 Q3", "Q3 2021"), months ("2021-03", "Mar 2021") and dates. A period missing from the data is a gap. The new argument prdord gives the order of any other labels; labels whose order cannot be read keep the old behaviour, with one warning that names them. yoysmry() and anltab() follow the same order, and ratechg() orders text periods the same way.fipsmod() loads the OpenSSL FIPS provider (a FIPS 140-3 module) on its own, with a configuration that cannot fall back to the default provider, and reports which module and build it is and whether the package knows a certificate for it; fipsmkg() makes an ML-DSA or SLH-DSA key inside that module, and cpssign() signs with it there. The signatures verify with cpsvrf() and cpsxvrf() as any others. The list of certificates (inst/fips/modules.csv) names the Chainguard FIPS Provider for OpenSSL 3.6.0-r4 (CMVP #5523, with ML-DSA and SLH-DSA) and the OpenSSL FIPS Provider 3.1.2 (CMVP #4985, without them); any other module is reported as not listed, never as validated.SECURITY.md follows the OSTIF best-practices guide: private reporting through GitHub (now switched on) or email, scope, what a report should contain, the response timeline (acknowledgement in 72 hours, triage in 7 days, a fix in 30 days for high and critical issues, then a GitHub advisory and CVE), good-faith research protection, the incident plan, who has access and what each CI secret can do.LANG=C.UTF-8; the Wolfi base sets no locale, so R in the image ran in C.cpsplant() checks that a statistic recovers effects of known size planted into the real data (the outcome shuffled to remove its real association, then effect * treatment added), so a capsule can show the analysis measures what it claims, not only that it re-runs. A planted 0 catches a statistic that finds effects that are not there.cpsspec() runs reasonable alternative specifications and reports whether they agree in sign with the reported one; recevid() writes a planted-effect check, a specification check or the falsification controls of cpsfals() into a manifest, so the capsule records which correctness checks ran and whether they passed.tsstmp() gets a public RFC 3161 timestamp for anything a study fixes before it sees its data: a ralloc() allocation list (its commitment is stamped), a file of declared outcomes, a manifest or a capsule. Only the SHA-256 leaves the machine. The token is checked at once with tsvrf() and can be saved; with the authority's root certificate as trust, anyone can later prove the commitment existed on that date. The default authority is freetsa.org, which is free and needs no account.cpsxvrf() checks an ML-DSA or SLH-DSA signature twice: with this package and with the openssl program (3.5 or later), which implements FIPS 204 and 205 independently. It counts as cross-verified only when both accept it; without OpenSSL, or for a scheme OpenSSL cannot check, the result says the signature was verified here only, and why. OpenSSL is never required.fipskg() makes an SLH-DSA-SHA2-128s key by default (it was ML-DSA-65): its security rests on the hash function alone, the most conservative assumption for a capsule meant to be checked decades from now. Signing takes about a second and the signature is 7,856 bytes; pass scheme = "ML-DSA-65" for the old default. XMSS stays opt-in through pqckg().A bundle run on synthetic data works on Windows: the runner passed BRICKLAYER_SYNTHETIC=1 to analysis.R through system2(env = ), which on Windows puts it in front of the command, so analysis.R never started and the run stopped with exit code 5. It now sets the variable for the child and restores it afterwards.
On a console that is not UTF-8 (R in a C locale), the result boxes fold accented text to plain ASCII, so "Métis" prints as "Metis"; before, R printed the escape <U+00E9>, which broke the box's alignment and its 80-column width.
Tests that compared a computed mean, variance, quantile or sum bit for bit with R's own allow a difference of 1e-12: platforms without extended precision (macOS on Apple silicon) round the last digit differently.
A gzipped table too large to decompress in memory was read a piece at a time without checking the gzip stream, so a truncated file came back as a shorter table with no error. The stream is now checked whole first (trailer, CRC and size), and a truncated or damaged file is refused, as smaller files already were.
The C function rmbl_wayback_snapshot(), which other packages call through LinkingTo, answers 0 for a NULL address or buffer, or a buffer of no size, where it crashed the calling session, and for an address that is not http(s) without asking archive.org.
The file readers refuse a directory by name ("path is a directory, not a file") instead of opening it.
wcb() refuses a covariate the fixed effects absorb (one that does not vary within units), naming it, instead of failing in solve() with "system is computationally singular".
Plots restore every graphics setting they change, colours included, and panel layouts restore their margins and text size; a test runs every plot example and checks the device is left as it was.
Errors name the argument as you typed it (for example diam, cl, lvl, msg), and the arguments that take one TRUE or FALSE, one number or one name refuse anything else instead of using its first element or ignoring it. expconcidx() refuses an income with a single value, where it returned an index of 0. capdeps() warns about a package that is not installed. envdiff() reports any other field that differs (fields). jsb64dec() refuses = padding before the end.
idcrd(), idcrdrd() and idcrdvrf() make, read and verify an identity card for a signing key: your public key (ML-DSA, SLH-DSA or XMSS) with your name and, if you give them, your ORCID iD, e-mail address, web page and affiliation, signed with the key itself under its own context. Publish the card and give its fingerprint where people know you; idcrdvrf() checks the card is intact and in date, that its fingerprint is the one you gave, and that an attestation or a bundle was signed by its key. No authority and no network are involved.
The examples of certprs() and certchnvrf() read a test chain shipped as files (inst/extdata/x509/) rather than certificates pasted as hexadecimal.
dload() reads a table of any size when its SHA-256 matches the manifest: it is then the file data.rmorie.com holds, so the 1 GiB limit on what an untrusted CSV may decompress to (option rmoriebricklayer.maxcsvbytes) no longer applies. Tables of about 95 MB of gzip and more decompress past 1 GiB and were refused. A table the manifest lists without a digest keeps the limit, and a gzip file over 2 GB is read a piece at a time.
The reference index lists the categorical-integrity functions once (the section appeared twice).
Vignettes, the README and the reference index spell Métis with its accent in their text and headings (the plots already did).
plotopt() refuses a grid, cex, las, bg or panel value the plots cannot use (for example grid = "x"); it used to store it, and every later plot of the session then failed.
The rule constructors (rulenull(), ruleuniq(), ruleincr(), rulemad(), rulebtw(), ruleinset(), rulerx()) give one message whether col is left out or NULL: "col must name a column: a single non-empty string".
missing() default to NULL; each R file has a test file of the same name.explerr() lists the exported functions an error is reached from through any helpers a function is split into, as well as up to four calls of shared code. Every line of R, of the tests and of the vignettes' code is at most 80 characters, and lintr::lint_package() reports nothing. The source is formatted with air (air.toml).plot.rmbl_prevalence() (its help page showed it twice), .bl_interactive(), .bl_stdin_tty() and .rmbl_permutations().<U+00E9>, so a manifest naming é.csv named a file that does not exist. They are written as UTF-8 bytes now, in any locale.mkbndlchk() with container = TRUE refused every folder on Windows, reading the drive's colon (C:) as a separator; only a colon or comma after the drive is refused now.mkbndl() counted files through a folder link (Sys.readlink() sees no link there), so a link to the project's own folder was followed for ever; links and junctions are found by their real path now. A bundle's runner whose provenance has no download address says so and stops (exit 5) instead of failing with an R error.mkbndlchk() no longer waits out its whole time limit when bash is not on the PATH (/bin/sh is dash on Debian and Ubuntu, which has no job control without a terminal): a one-second run took the full limit and reported a timeout.mkbndlchk() stops only what the run left behind: a process the user's own job (for example parallel::mcparallel()) started during the run is left alone, and a user systemd service is stopped only when its process carries the run's mark.mkbndl() refuses file names mkbndlchk() would refuse (a backslash, or two names that differ only in case), mkbndlchk() refuses a zip that unpacks past 512 MiB (option rmbl.bundle.maxsize), and a bundle's runner refuses an unknown argument, a bare --data and a folder given as --data.<U+XXXX> instead of printing blank.ralloc() ratios, counts, digits), functions or environments where data belong, and NA where names belong now give clean errors.openssl is tried before the pure-R digest, which is kept to files of 4 MiB.audcats() reads haven-labelled columns (also inside I()) by their codes, so it no longer fails with the vctrs version on macOS.rallocvrf() refuses a function as seed without a warning first.mkbndlchk() also lists the user's running systemd services before and after the run, so a bundle that starts one with systemd-run --user is stopped and fails the check.rptanl()'s Markdown report escapes control bytes in its title and table cells, and writes each number on its own (20, not 20.00000).mkbndlchk() uses when it runs a bundle (unshare, systemctl, podman or docker).mkbndlchk() makes R the run's subreaper and compares the process list before and after, so a process that leaves the session or clears its environment is still found and killed, and fails the check. Where escaped processes cannot be detected (macOS, or very old Linux kernels) a check without container = TRUE fails with a message saying so instead of passing.mkbndl() counts a project's files before checking or copying any of them, so a project too large for a zip is refused in seconds. Documents (.md, .Rmd, .qmd, .html, .pdf and similar) are no longer left out because their names contain a word such as "key" or "credentials".mkset(), yoymd(), yoyhtml(), anltab(), capsule checks and dashboards) escape terminal control bytes from labels and headings as the console output already did.*_SK names and URLs with a password but no user name.registerS3method() turned the method table of the namespace into a list and the lookup failed.%XX sequences, so a hand-edited capture can no longer turn an upload key into another item's path or an ?acl request. The same fix covers the archive lookup, the Wayback save form, CKAN searches, wbsnpurl() and model names in language-model addresses.mkbndlchk() treats a//x and a/./x as the same entry as a/x, so a zip can no longer carry a second runner under an equivalent name.mkbndl() leaves out more secret files by name (any name with a word such as key, token, secret, credential, password, pat or adc, plus .env files and kubeconfig); data tables and .pub files are kept, and secrets = TRUE packs everything. It also refuses a bundle with too many files before copying anything.tools::sha256sum(), then the operating system's tool (sha256sum, shasum -a 256 or certutil). The pure-R digest is used only for files up to 50 MB; larger files stop with a message instead of running for hours on R older than 4.5.mkbndlchk() passes only real locale variables (LANG, LC_ALL, LC_CTYPE and the other categories) to the bundle, drops more secret-looking variables on Windows (DATABASE_URL, webhook and DSN addresses, URLs with a password), and fails the isolation check when a process outlives the run on systems without a PID namespace.certprs(), certchnvrf() and name-constraint checks show bytes that are not valid UTF-8 as <xx> instead of failing with "invalid multibyte string".ralloc() refuses arm and stratum names that are blank after removing Unicode spaces such as U+00A0 and U+200B; arccap() and arcpush() take visibility only in full.yoy(), ratechg() and analyse_table() no longer warn in a C locale when group labels are not ASCII.fetchvrf() archive lookup is mocked and the morie cross-check skips on CRAN. A sweep of all 238 test files with every outside connection blocked and logged found nothing else.mkbndlchk() reads only the results folder its own run created. A results_* folder packed in the zip, a run that writes no manifest, and a runner that says "NOT completed" all fail the check.mkbndlchk() and rmbl pack --check run the bundle with a minimal environment and a temporary HOME and TMPDIR, so no API keys, tokens or passwords reach it. The run is contained and every process it started is killed when it ends, times out or is interrupted: in a PID namespace on Linux (even setsid daemons die), in a job object on Windows, otherwise in its own process group. The mode used is shown as isolation. The default time limit is 30 minutes (rmbl.bundle.timeout), and both warn that they run the bundle's code.mkbndlchk(container = TRUE | "podman" | "docker") and rmbl pack --check --container run the bundle in a locked-down container: read-only bundle, no network unless the real data must be downloaded, memory, CPU and process limits, a non-root user, removed afterwards. The image is a pinned Rocker R 4.6.1, overridable with options(rmbl.bundle.image).mkbndlchk() refuses zips with duplicate entries, NUL, over-long, absolute or .. entry names, or zip64 counts, and reads a large zip's directory in linear time (70,000 entries in under 2 seconds).data/ folder. Every file a reviewer supplies is checked against the pinned SHA-256 (exit 7 unless --allow-digest-drift).setup_and_run.R stops with a clear message (exit 2) on a malformed or duplicate-key config.json, exits 8 when the analysis writes no manifest, and a refusal raised inside a bundle no longer fails with "not a namespace".mkbndl() refuses a project$name or version that is not a plain name, does not follow symbolic links out of the project, and leaves out files that look like credentials unless secrets = TRUE (rmbl pack --secrets). It writes the zip under a temporary name, so an interrupted build leaves no partial zip, and refuses more than 65,535 entries.arcpush() refuses manifest file names that leave the capture folder (.., absolute paths, backslashes, control characters, symbolic links out), and percent-encodes S3 keys and mirror URLs within the one archive.org item.arccap() and arcpush() check the time-stamping authority's reply with tsvrf() and the request's nonce before keeping it; a junk reply is not saved, and a junk manifest.tsr kept by an earlier version is set aside as manifest.tsr.rejected and stamped again. The capture summary no longer prints "imprint and signature verified" for a token that did not verify, and one private address no longer stops the Wayback Machine request for the other files.tsvrf() reads the unsigned parts of a token strictly (SignedData and SignerInfo versions, digestAlgorithms against the SignerInfo digest, the signature algorithm against the certificate key, the class and form of every field, every certificate the token carries): of 26,054 byte mutations of the test tokens, none verifies except ones shown harmless.certprs() checks the whole certificate layout (RFC 5280 4.1), refuses an outer signatureAlgorithm that differs from the signed one and a malformed time, and refuses every malformed certificate in words, not with an R error.mkset() headings and progress labels.plot(ratechg(..., prdord = )) follows prdord, and the ratechg() box prints whole-number periods as labels (2020, not 2020.000).plndid(), evstudy() and loounit() refuse a data that is not a data frame, in words; weights large enough to overflow (w = 1e308) give the same estimate as equal weights.yoy() folds labels a C-locale console cannot show, as rtcalc() does.mkset() no longer embeds a data frame's pairs plot. ralloc() refuses blank arm and stratum names and names that differ only in spaces. vrfmrg() refuses an empty or NA pub.cpsplant() allows at most 1.5 times tol (one Monte Carlo standard error, which must be at most tol/2): an estimator attenuated to 0.7 with moderate noise now fails. The default n is now 100 (was 50), so a difference in means on about 120 rows is no longer reported as too noisy; a noisier statistic may need a larger n.tsvrf() reads an RFC 3161 token by position, as RFC 3161 and CMS lay it out. A token wrapped in another structure, a response whose PKIStatus is not granted, digest algorithms with parameters other than a DER NULL (outside the signed bytes, so they made a token malleable), and a SignerInfo naming a certificate other than the one checked are refused. The signed attributes are read among themselves; a messageDigest nested inside another attribute was taken before. New argument nonce: given, the token must answer that request. tsstmp() and arccap() check the nonce they sent (arccap() drew it from R's random numbers; it now comes from the operating system).cpssign() loads the module set up now (options rmbl.fips.openssl and rmbl.fips.conf) and refuses a key from another. The module's status is described as its own report. Every openssl run, the cross-check's too, stops after rmbl.fips.timeout seconds (300); a program replaced during the session is read again. fipsmkg() makes "SLH-DSA-SHA2-128s" by default, as fipskg() does.rmbl verify --public-key and cpschk(pubkey =) take hexadecimal only (16 digits at least); " " and "x" were accepted and checked nothing. A key pinned against a capsule with no signed bundle fails it, so deleting a bundle no longer passes a pinned-key check.ralloc()'s commitment enters each field with its length, so settings joined alike by a comma (a stratum "x,y" against "x" and "y") no longer share a commitment; lists made earlier keep theirs and still verify. rallocvrf()'s seed check compares the whole list (ids, strata, blocks, kit codes), not the arms alone. Refused with a message: a seed that is not one value, block sizes that are not finite or past 1,000,000, empty arm names, n past 10,000,000. Kit codes for large trials are drawn in linear time, the same codes as before.plndid(), evstudy(), mtchdid(), plndml() and plnpois() warn when an explicit cl gives fewer than ten clusters (CR1 errors are unreliable; wcb() is made for that case) and when a cluster changes within a unit; the estimators that read one cluster per unit refuse it. wcb(), perminf() and plnpwr() do not warn.plndid() and evstudy() (a unit of them failed with "missing value where TRUE/FALSE needed"); infinite weights are refused there and in cwtd(), sprd() and the Indigenous mental-health domain weights (cwtd() returned NaN for them, and for all-zero weights).svydes() takes negative replicate weights, as calibration (GREG, raking) produces and survey's svrepdesign() accepts; the full-sample weight stays non-negative.trndtst()'s reading claims a direction only when the test supports one ("leans downward, but p 0.483" where it said "a downward tendency"). Overlapping intervals are no longer read as "the data cannot tell the two apart": the difference has its own, narrower interval.ratechg() and anltab() take prdord; in ratechg() a label the order lists that the data lacks is a gap. ratechg(), rtcalc(), shrs() and yoy() refuse an argument they do not take, by name, where it vanished into ....staysmry() and the other Lakner measures name the function and the argument in every refusal.rmoriebricklayer.maxgzratio), in drift, the data hub and the pollution readers: a 1 MB reply that inflated to just under the 1 GiB cap took 2.7 GB to read. Files verified against the data service's manifest are not capped.yoy*() writers, wrmnfjs(), wrtxtfb(), mksyncsv(), mkset(file =), idcrd(), drfthtml(), tsstmp(), bldl(), blfetch(), fetchvrf(), arcget(), siufetch(), dldata(), fdl() and cpsbndl(). mkbndlchk() stops a run after rmbl.bundle.timeout seconds (7200).rmbl verify (and cpschk() in verify and report mode) refuses a provenance whose source is a local path outside the capsule, or a file:// URL without allowfile, as refresh and drift do.cpschk()'s Markdown and text reports and the categorical guards' messages use ASCII words and quotes where they printed <U+2714> and <U+2018>.login set openai.model gpt-4o?). The cache key of saved answers uses the same length-prefixed fields as the commitment.arccap() gains upload, reason, visibility and warc. A capture made from your own connection (the way to keep files from servers that refuse the Wayback Machine, Statistics Canada among them) is written as a WARC/1.1 file with a CDXJ index, the layout pywb and warcio read, and with upload = "rmorie" is kept at archive.rmorie.com under your data key (dlogin(), rmbl login); upload = "wayback" asks the Wayback Machine to save the same addresses, and both may be given. Captures are public unless visibility = "private".arcfind() lists the captures of an address (or of every address under a prefix ending in *), oldest first, with their SHA-256; arcget() downloads one capture's exact bytes, by its id or by address (the latest, the one closest to at, or the one with a given SHA-256), and deletes a file whose SHA-256 does not match.fetchvrf() gains arcs: after the source, its Wayback snapshot and the mirrors, it tries archive.rmorie.com and then folders written by arccap(), reading the payload back from their WARC files; every copy is still checked against the pinned SHA-256.wbsave() gains fallback = "rmorie": the addresses the Wayback Machine refused are saved from this connection to archive.rmorie.com instead (attribute archive of the result).cpschk() gains archive (the rmbl action's archive input): in drift and refresh, a source that neither answers nor has its pinned Wayback snapshot is fetched from the copy kept on archive.rmorie.com with the pinned SHA-256 (public copies need no key), and with wayback = TRUE a changed source the Wayback Machine refuses is saved there instead.id-token: write, saving to archive.rmorie.com needs no stored key: without one, the package asks GitHub for the job's short-lived OIDC token for archive.rmorie.com, and the repository is the saver.archive save URL... --reason TEXT [--private] [--also wayback], archive find URL and archive get ID -o FILE.MORIE_ARCHIVE_URL, with its own key in MORIE_ARCHIVE_KEY; the stored rmorie key is sent only to archive.rmorie.com.URL, BugReports). The CRAN build of 1.0.0 left them out, so its CRAN page linked to neither and r-universe could not match the CRAN release to its repository.citation("rmoriebricklayer"), CITATION.cff and the README cite the package at its CRAN address.bldl() asks a server that is busy for a moment (HTTP 429, 500, 502, 503 or 504) again up to three times, after the wait its Retry-After header asks for (at most a minute) or else 1, 2 and 4 seconds, instead of failing at once. data.rmorie.com answers 503 when many clients fetch one table at the same time, as every CI job of a push does.$ in a recipe started a formula and a | ended the cell, so most rows ran together as mathematics.?baltab quotes its title with \sQuote{}, so the PDF manual prints an opening and a closing quote around Student's instead of two closing ones.srchfig() with no argument gains a column yours: for each of the 36 figures, one line of R that draws it from your own numbers (the function behind the figure, with example inputs to replace; a schematic gives the computation it pictures). The search-and-rescue guide prints the table, and a test runs every line.pkgload::load_all()), because it read only the installed help pages; it now falls back to the source man/ folder, as the command line already did..packages(all.available = TRUE)) instead of reading every package's metadata with installed.packages().srchfig("4-4") and srchfig("4-6") left the device split into panels, so the next plot was drawn into a fraction of the page (figure 4-7 in the search-and-rescue guide came out squashed). Every figure now restores the layout it changed, and a test draws all 36 in a row and checks it.co2fp() measures a computation's footprint is now named after the function that asked for the footprint (for example cpschk: the provenance names no source to fetch), not after co2fp().R CMD build), so the articles show their plots; only R CMD check re-building a vignette skips the computing.R_NO_REMAP, and the size checks use C++ static_assert.rmbl command line translates them instead, with the new verbs run and migrate, and describe and examples now answer for an old name. Every exported function has an example.fromjs() for the JSON parser's errors, the URL validator for host classification, the credentials reader). Two statements after a return() or break were rewritten so that no code is unreachable.format() lines share one function, defined with them in one file, instead of 178 copies of the same body; yoy_summary() has one method where two were identical.goodpractice} runs: %||% is defined once (it is base R from 4.4.0, and defined only for older R) instead of in four files; print() of a 0.5.x-form result returns it invisibly; two on.exit() calls add to the exit handlers instead of replacing them; the four test seams (the AES and HQC backend switches, the running mean, the services memo) live with the tests instead of in the package; the DESCRIPTION no longer uses two dependency names as ordinary words; and a test helper named dbl(), which {lintr} mistook for rlang::dbl() and crashed on (so that none of its checks ran), is now hexdbl().anltab(period =, rounding =), recrd(observed =, expected =, synthetic =), wrsmrytxt(what_was_done =)) and one read a renamed field ($any_drift, now $anydrft, which printed NULL); every R block of the README now runs as written.\dQuote{}, so the PDF manual prints an opening and a closing quotation mark instead of two closing ones, and quoted values are in code font (\code{"mean"}). A percent sign written in a help page's text no longer cuts off the rest of its line (in 11 pages, for example the 50% rule in ?cnsprof, which read "50" and stopped).inst/WORDLIST; spelling::spell_check_package() finds nothing.vignette("capsules-and-provenance"), vignette("integrity-and-cryptography"), vignette("describing-data-and-tables"), vignette("statistical-methods") and vignette("applied-analyses"), each former guide a section of one of them, with its text, code and figures unchanged. The package from GitHub or r-universe, and the package website, keep all 33.example()), and the full test suite runs in GitHub CI and on r-universe. On CRAN, to keep the check within its time limit, the help pages show every example but R CMD check runs only those of csha256() and keyeddgst(); the tests run only off CRAN.plot() methods and plot helpers from the objects each vignette builds.rmblapp() opens a dashboard over the package's analyses on your own machine (rmbl app [PATH] [--port N] [--no-browser] from a terminal; it needs shiny, which is suggested, not imported).
Load a CSV file, a capsule's pinned data or a data.rmorie.com table, then describe, compare
years, compute rates, test trends, estimate from a survey, fit distributions, check a capsule,
evaluate a policy on a panel, read the First Nations, Métis and Inuit tables, work through
courts and corrections counts, model a plume and its burden, fit a Hawkes process, or check a
proof ledger. Each analysis shows the R call it ran and runs exactly that call; a refusal is
shown in the package's words with explerr()'s note. The Report tab gathers results into one
HTML page (plots as inline SVG), Markdown or a GitHub comment. vignette("dashboard").rmbldemo() returns seeded, made-up datasets for the dashboard and examples: a court caseload
panel with a staggered reform, monthly offence counts with a step change, a stratified survey
with replicate weights, a published health table with suppressed cells, custody stock and flow,
a monitor series around a plume source, a Hawkes event stream, two drift snapshots, case
processing times and a proof ledger.dstfit() fits normal, log-normal, exponential, gamma, Weibull, Student t, Pareto, beta,
Poisson, negative binomial and zero-inflated Poisson and negative binomial distributions by
maximum likelihood in base R, ranks them by AIC with BIC, Kolmogorov-Smirnov and
Anderson-Darling statistics, gives percentile bootstrap intervals and a Hill tail index, and
plots the density, QQ, PP and log-log tail. mksyncol() takes type = "fitted" to draw from a
fit.plotrdg() (ridgelines),
plotecdf(), plotvln() (violins), plothex() (hexagonal bins), plotgain() (concentration
curves), plotcal() (calendar heatmap), plothrz() (horizon chart), plotsm() (small
multiples) and plotbump() (ranks over time). plothwk() draws a fitted Hawkes process's
intensity, time-rescaling check and most probable branching; chwksim() simulates from it.ldgrread() and ldgrchk() read and check a proof ledger (schemapath("ledger")): each theorem
proved with no sorry, each covered function exported by the installed package, each numeric
check within its tolerance. A check is evaluated in a sandbox that allows only the named
package's functions and base arithmetic, so system() or a file read is refused before it runs.
A capsule whose provenance says kind: "ledger" gets these rows, and its commit and build pins,
from rmbl verify.srr tags: 149 standards tagged as met, 29 not applicable, none left to do.plot(yoy(...)) carries its whole reason again ("first period:
nothing to compare with", "no percentage: previous period is zero"), sized to the panel; the period-axis
rewrite had cut every reason at the colon.Date; text at labelled
positions) with the unit as the axis title, and take xlab, which passed through ... was an
error. plot() of a yoy() or ratechg() result draws the period across and the change up, with
vertical interval bars; groups sit side by side. gaps = "break" (the default) breaks a line at a
missing value, such as a "no data" year yoy() inserted, and gaps = "join" draws over it."anltab: data must be a data frame"), and so do the shared argument checks.stepchg() puts before, after and difference back into the units of y when the
series is above 1e150 (they stayed rescaled), rescales a series below 1e-150 as well (the
squares underflowed and every split tied), and refuses an x that is not numbers or dates, has a
missing period or is not strictly increasing. trndtst() refuses duplicated periods.yoy() compares yearly, quarterly and monthly Date periods (all on one day of the month, or
all month ends) on a grid of calendar months; on a grid of days none of them had a comparison.svydes(type = "jkn") on a domain keeps a replicate that deletes a PSU of a stratum of 101 or
more PSUs: its factor, within 1% of the weight, is the 1 / rscale its rscales says, and it
was refused as a copy.plot(yoy(...)) says "no data in the previous
period" for a period after a gap, which it called the first period, and refuses a result with
no values.rtcalc() and ratechg() name pop in their refusal, and exprspplr() names the call
estr(data, y, expo, confs). blcli() refuses NA or a function as args, dlogin() a
function as email, and instcli() a dir it cannot create or write, each with its name in
front.blcli() and explerr() run only in an interactive session.chwkfit() warns when the branching ratio sits on its 0.99 bound.yoy() compares ordered-factor periods by level order and completes missing levels; its
refusal names every accepted input. fylbl()'s help says a fractional year is an error.@noRd (srr G1.4a).cex (?blpal).cpsatst(), cpsbndl() and their checks take a shared secret (HMAC, the attestation recording only its key id), so bundles signed by morie verify with cpsbndlvrf(key = ).vrfcps() reports a provenance record that does not parse as a failed provenance_readable
row with the parser's message, instead of stopping; the other rows are then run as for a
missing record. morie.capsule.verify() answers the same way.
usecpstmpl()'s analysis.R pins the source digest into data_provenance.json on its first
run (source$sha256 and a resource block), so vrfcps() verifies the capsule from then on.
Before, the provenance said the digest would be filled in and nothing ever did.
cpschkatst()'s key_as_expected row says whether the key matched; its detail read the same
either way.
The Docker image installs the package and puts rmbl on the PATH.
capenv()$packages is a named list, so a manifest written with wrmnfjs() keeps the package
names; as a named character vector they were written as a bare array of versions.
plot() of a cpsdrft() result takes what: "psi" (the default) draws the PSI per column,
worst first, coloured by verdict with the warn and fail bands as dashed lines; "columns" draws
the reference and current distributions of up to six columns (cols) with each PSI bin's term;
"benford" the first digits of numeric columns against Benford's law with its 95% band; "ecdf"
the two ECDFs, "quantiles" the quantile shift and "fingerprint" the PSI of every column on
spokes, the bands as rings.cpsdrft() keeps a binned summary of each column (summ: missing shares, summary statistics or
the largest categories, the PSI bins, a histogram, first-digit counts; never the rows) and its
PSI bands, which the plots and reports colour by.drfthtml() writes the drift report as one HTML file that fetches nothing: the verdict and
provenance; the chosen column's two distributions drawn in on a canvas; a column table to search,
filter and sort; per column its statistics and the density overlay or category flow, ECDF
overlay, quantile shift, PSI by bin and Benford charts, each with a line on how to read it; the
drift fingerprint and a tile per column; the manifest diff as a tree; PSI over earlier runs as a
heatmap; the capsule chain (provenance, bytes, manifest, attestation); sliders for the bands and
alpha that score every chart again; and Markdown, JSON, SVG and GitHub-comment exports. Dates show
in ISO 8601. rmbl drift PATH --html FILE writes it.drftapp() is the same report as a Shiny app (shiny is suggested), with what would clear a
column, a rerun on chosen columns and a download of the page; drftapp() alone opens on two
uploaded CSV files, and rmbl drift PATH --app opens it on a capsule whose source moved.
cpschk() keeps each capsule's cpsdrft() result in its drifts attribute.The checks the rmbl GitHub Action ran in its own script are now in the package, so the action can call them.
rmbl verify PATH checks every capsule under PATH: each capsule file against its JSON Schema,
then vrfcps() and cpsbndlvrf(), one row per check, with the action's Markdown report.
--json prints the rows as JSON instead, and --fail-on error|drift|warn sets which failed
row makes the exit status 1. The secret of an HMAC bundle is read from RMBL_KEY, which is
removed from the environment at once; --public-key pins the signing key.rmbl drift PATH verifies, then fetches each source again: new bytes are a warning, and
columns that drift (cpsdrft()) are drift. rmbl refresh PATH pins the new bytes, and
rmbl report PATH [--format md|json] prints the verify report and never fails.inst/schemas/ (the same files as the action's spec/); schemapath()
gives their path.mnfdiff() lists the resources two manifests (provenance records, bundles or data frames)
added, removed or changed, with their digests and sizes.cpsrfrsh() fetches a capsule's source again and, when the bytes changed, writes them and pins
them in the provenance as usecpstmpl()'s analysis.R does; it returns the mnfdiff().dload() takes token, a MORIE key for that call that is used in place of the stored one and
never printed. (key is already the table's name.)explerr() takes a data frame of check rows, as rmbl verify --json writes them, and explains
each failed row.cpschk() runs these checks from R, and with them everything the action's script computed on
its own: PSI bands (psiwarn, psifail; --psi-warn, --psi-fail) with the action's verdicts,
the per-column drift table with the Benford first-digit p-values of the pinned and the new data,
each manifest compared with its base copy (base, --base: a manifest file, a folder, or a
function that resolves a git ref; results through mnfrcmp(), resources through mnfdiff()),
a cache of fetched sources read when a fetch fails (cache, --cache; none outside an
interactive session unless a folder is given), file:// sources (allowfile, --allow-file),
Wayback snapshot rows (snapshot, --snapshot) and captures on refresh (wayback), a glob over
the capsule files (glob, --manifests), explerr() notes on every failed row, and the
footprint of the checks (co2fp()) at the foot of the report (fp, --no-footprint).
format() renders the Markdown report or the JSON.ghann(), ghcmt() and ghout() turn a cpschk() result into GitHub workflow annotations (the
file, and the line of a schema problem), the body of a sticky pull-request comment and the
action's step outputs. They return text; nothing in the package calls GitHub.rmbl drift, new bytes whose columns all stay within the PSI bands are a note rather than a
warning; new bytes that cannot be compared column by column are still a warning.cpsrfrsh() records resource$retrieved_utc, takes allowfile for a file:// source, and
refuses new data that fail the schema the provenance pins.rmbl sign PATH pins each plain provenance on its first run and signs each capsule into
capsule_bundle.json (cpsseal()), then verifies, so a program in any language only writes
the data and a provenance naming it. The key is RMBL_KEY or the contents of --key-file,
never printed: the shared secret for --scheme hmac (the default), or for a standardised
scheme the JSON of a fipskg() key or its secret hex with --public-key. An existing bundle
is refused without --overwrite.rmbl sign --scheme xmss-sha256 --key-file key.json signs with an XMSS key and treats the key
file as its state, as RFC 10033 asks: before anything is signed the file is rewritten
atomically (a temporary file beside it, flushed and renamed, under a lock) with next_index
advanced and a used field holding the highest index used, a generation counter and a digest
of the host that wrote it. A key file that cannot be rewritten is refused; --state-file
keeps the state elsewhere, and a key in RMBL_KEY needs it. --sectors K --sector-index I
(or --sector NAME) signs only in one of K disjoint index ranges, --reserve N reserves N
indices in one write, --warn-left N warns when the range is nearly used up, and a key file
older than a signature it is seen to have made (in a bundle under the path, or in the
--seen-log each run appends to) is refused as a stale copy. The command line never writes
to the configuration directory; RMBL_XMSS_INDEX_FILE is used only when it is set.rmbl verify and cpsbndlvrf() name each XMSS signature's index and key fingerprint, and an
index one key signed twice under the path or in --seen-log (cpsbndlvrf(seen = ),
cpschk(seenlog = )) is a failed xmss_index row. xmssidxseen() lists the XMSS signatures
in a folder of bundles and seen-logs and marks a reused index. cpsseal() signs several
capsules with one XMSS key, at consecutive indices.rmbl refresh as a file:// URL.plot() of a rtcalc() result with period draws the period on its own scale, as the other
plots over time do, and takes xlab; it drew index positions with no axis title.Every exported function, argument, output column, help topic and option now has a short name without
underscores (survey_design() is svydes(), conf_level is conflvl, trend_sensitivity() is
tse()). Each help page gives the full form of every short name. The 0.5.13 names that 1.0.0 renamed are known to the command line, which translates
them: rmbl describe admissions says what the name became and shows its page, rmbl run admissions --adp 120 --alos 30 calls the function by its old name with its old argument names and values, and
rmbl migrate script.R rewrites a 0.5.x script to the 1.0.0 names (--write writes it). Renamed
allowed values ("mean_bootstrap" is "meanboot", "higher_is_better" is "higher",
"all_cause_mortality" is "allcause") and old argument names passed through ... are
translated too. In R, the eleven functions rmoriedata 0.3.x calls keep their old names
(?rmbl-compat), and so do the four S3 generics below; ?rmbl-aliases lists every renamed value,
column and field. The nine options read their 0.5.x names too (rmoriebricklayer.xmss_index_file for
rmoriebricklayer.xmssidxfile), so a record or cache an old .Rprofile points to is still used. rate(), rate_change(), share() and
yoy_summary() stay S3 generics, so methods written for them still dispatch. yoy() kept its name and
takes its 0.5.x argument names (period, min_base, conf_level, direction) beside the new ones.
Results of the four generics come back under their 0.5.x
column and field names (co2e_g, pct_valid, nested tables and attributes too, and the header row of a
delimited writer), and print(), plot(), format() and summary() work on them. A 1.0.0 call returns the 1.0.0 names. The one call that cannot tell is yoy(),
which kept its name: with a 0.5.x argument name it returns the 0.5.x names, without one the 1.0.0
names (pctchg for pct_change); ?rmbl-aliases lists every renamed column and field. rmbl functions and the help-aware language-model
tools list the new names. Wire formats (capsule, manifest and service JSON fields, CSV columns from
published tables) keep their names, so files written by earlier versions still read.
Methods for the published tables, distinctions-based (First Nations, Métis and Inuit are each reported on their own), with suppressed cells never imputed. Data governance follows OCAP® and the CARE Principles: only openly licensed aggregate tables are used, and First Nations-held survey data are not mirrored. A new vignette, "First Nations, Métis and Inuit health: published tables, distinctions and governance", covers them all.
Reading the tables
sctidy() reads a Statistics Canada table as published. VALUE becomes a number, and the
quality flag is kept beside it. F, x, .. and ... cells are NA.scests() puts each estimate beside its confidence limits and count.indtabs() lists the tables the data service mirrors: the 233 Indigenous-titled
Statistics Canada tables, and the 2021 Census Indigenous Population Profile.indiex() bundles four published excerpts (tables 13-10-0924, 41-10-0067, 41-10-0082
and 41-10-0021), so the examples run on real values.Comparisons
indgap() gives the difference and ratio against a reference group.dstntst() tests whether the groups differ before they are pooled (Cochran's Q and I²).ordcmp() compares rated scales, such as perceived mental health or life satisfaction,
as whole distributions: the probability of a higher rating, with an interval from the published
limits.agestd() and rategap() cover counts. stdprev() covers survey
percentages.genexpo() and exptrnd() cover the intergenerational effects of the
residential schools.trcind() maps each indicator of Call to Action 19 (health gaps) and Call 55 (children
in care, education, income) to the tables that measure it. trchlthgap() measures every
gap against non-Indigenous people on one scale, "times worse", with intervals. Life expectancy and
infant mortality (tables 17-10-0160 and 17-10-0161) are bundled for it.trccmpl() and trcprog() track the 94 Calls to Action, as the Yellowhead
Institute counts them (Jewell and Mosby).Estimators built from Indigenous scholarship on mental wellness
These translations are ours. They are inspired by the scholars' work, which is largely qualitative, and are not taken from it as stated methods. Each help page says so, and says what the estimator cannot show.
welprof() reports wellness beside distress, strengths first (Stewart 2008, 2020).holbal() measures how evenly wellness is held across a person's domains: the ratio
of the geometric to the arithmetic mean, with community-set weights and Monte Carlo intervals
(Stewart 2008; Gabriel, Mirza and Stewart 2022).dualcont() treats wellness and distress as two axes (with Keyes 2002, 2005).healchg() reads change between survey periods as healing in process (Stewart 2008).connassoc() measures connection to land, community and education across
places: Spearman's rho with an exact permutation p-value, and an interval that carries the
published sampling error (Mashford-Pringle and Stewart 2019, Akiikaa; Mashford-Pringle 2016).mhcont() covers the First Nations Mental Wellness Continuum's four
directions (hope, belonging, meaning, purpose) from published proxies. A direction with no
proxy is said to have none.commidx() combines protective and risk factors with weights the community sets, after
the National Inuit Suicide Prevention Strategy. The function never chooses the weights.cwbscr() computes Indigenous Services Canada's Community Well-Being index.frmaud() gives the share of a report that is deficit-framed (Walter's 5D data).The 2021 Census Indigenous Population Profile
cnsprof() reads the profile. It keeps the characteristic hierarchy, the full-count or 25%
sample of each topic, the residence (on or off reserve), and the suppression symbols. Repeated
labels are told apart by their member number.profshr() gives shares (for example foster children among children, or speakers of an
Indigenous language) with intervals that carry the long-form sample and random rounding.cnsstd() gives an Indigenous standard population for age standardisation.cnscwb() computes a Community Well-Being score from the public profile, and says where it
departs from the Department's method.indiex("census_population") and "census_households") make every
example and test run on published values.Every result draws itself with plot().
Any agency's table, and the review of every estimator
limfrom() builds limits from a standard error, a coefficient of variation or a margin of
error, and qltyrcd() maps another agency's flags onto suppressed and use-with-caution.
An unknown flag warns and counts as ok.indgap(), ordcmp() and dstntst() take pooled_groups; the
output column pan_indigenous is now pooled_group. Duplicate groups are refused, and a total
as the reference warns.scests() turns a coefficient of variation into log-normal limits, which stay above 0.holbal() refuses proportions and gives a bias-corrected interval that holds the
estimate.stdprev() takes scale (100 by default, Inf for rates).genexpo() takes partial: a known attendance beside an unknown one is NA by
default, or 1 as a lower bound.connassoc() gives a Monte Carlo p-value of (b + 1) / (R + 1) beyond eight places;
its band is documented as a sensitivity band.healchg() takes distress; other indicators are marked "other".commidx() keeps its conf_level.profshr() takes rounding_base; on reserves, where the long form was a full canvass,
there is no sampling term.cnscwb() matches places by DGUID and takes min_population (250 by default; the
Department uses 65). Its doc says that housing is pooled across Indigenous households.cnsstd() takes a DGUID where place names repeat.trchlthgap(): times_worse is now disadvantage_ratio. It takes reference, and
warns on duplicate rows.rategap() refuses a group against itself; exptrnd() refuses an outcome with no
variation.Fetching what the Wayback Machine cannot capture
fetchvrf() fetches a file from its source (with the source's own Wayback fallback) or
from any mirror, and keeps only a copy whose SHA-256 is the one pinned. For servers that refuse
the Internet Archive's crawler (Statistics Canada does), a capture made elsewhere and published
as a mirror serves the same verified bytes.arccap() makes that capture: it fetches each URL from here, records its SHA-256 in a
manifest, timestamps the manifest with an RFC 3161 authority (verified with
tsvrf()), and uploads the files, the manifest and the timestamp as one public
archive.org item, streamed from disk with a new libcurl PUT. Given a wbsave() result it
takes the URLs whose server refused the Internet Archive.Evaluating a policy from administrative panels
New estimators, each checked against the reference implementation (fixest, did,
bacondecomp) to rounding error, in base R:
plndid(): difference in differences with two-way fixed effects and CR1 cluster-robust
errors (one-way or two-way), weights, linear combinations with delta-method errors (the
effect after a phase-in), and the effect relative to the comparison units' pre-period mean.evstudy(): leads and lags around adoption, with the joint Wald test of the leads.stgatt(): group-time effects for staggered adoption (Callaway and Sant'Anna, with
not-yet-treated or never-treated comparisons; or Wooldridge's extended two-way regression),
aggregated overall and by time since adoption, with a cluster bootstrap.bacondcmp(): the two-way estimate split exactly into its two-group comparisons.swtdid(): de Chaisemartin and D'Haultfoeuille's estimator, with its placebo.perminf() and stpdnpv(): Fisher's test by re-assigning adoption
dates across clusters (every assignment when there are few), and Westfall-Young stepdown
p-values for several outcomes.plnpois(): fixed-effects Poisson or negative binomial (NB2, base R) for counts, as rate
ratios.synctl(): one treated unit against a convex combination of donors, with the
placebo-in-space test.sevidx(), piperate(), cmlrisk() and bufasgn(): the
measures these designs take from justice data.polex() and the vignette "Evaluating a policy from administrative panels". The
designs are those of Professor Angela Zorro Medina's empirical work.stgatt() takes covariates: doubly robust, IPW or regression difference in
differences per cohort and period (Sant'Anna and Zhao 2020), equal to did::att_gt().perminf() and the wild bootstrap take ties ("count" or "strict").Causal estimation on panels
plndml(): double machine learning with unit and period fixed effects (Clarke and
Polselli 2025), cross-fitted by unit, with base-R lasso, ridge or OLS learners or any
learner function, a second-order basis built before demeaning, repeated splits, and
cluster-robust errors; equal to DoubleML given the same folds.aipwcf(): cross-fitted augmented inverse probability weighting for a binary
treatment (average effect, or effect on the treated) with survey weights and propensity
clipping; the average effect equals DoubleML's interactive regression model given the same
folds. With design = svydes(...) the standard error comes from the design's
replicate weights (any replicate type) or strata and PSUs, from one fit: the learners are
fitted without the weights, so each replicate estimate is the replicate-weighted mean of the
same cross-fitted scores, equal to re-running on every replicate weight.mtchdid() and baltab(): matching on pre-treatment trajectories (Mahalanobis,
pooled within-group or full-sample covariance, exact groups, calipers), then the weighted
difference in differences; matches equal MatchIt's.syndid(): synthetic difference in differences (Arkhangelsky et al. 2021) with exact
weights, placebo or jackknife errors.wcb(): the restricted wild cluster bootstrap with Rademacher or Webb
weights, full enumeration, and an interval by test inversion that is unbounded when the
design cannot reject.tse(): Rambachan and Roth's relative-magnitudes bounds with a conservative
interval and the breakdown value.plnpwr(): design-based power and the minimum detectable effect on real data.plotevgrid() for several outcomes, the Goodman-Bacon scatter, permutation
distributions, loounit() and cohtrnd().sprd(): the variance, standard deviation and standard error with the correction the
data call for (Bessel's n - 1 for a sample, none for a whole population described as it is,
the finite-population correction for a sample drawn without replacement, frequency or
reliability weights), chosen automatically from the population size and explained.ltrlrng() (lateral range curves), swpwdth()
(sweep width), srchpod() (probability of detection from coverage), pswppod() (a parallel sweep,
with navigation error), srchscs() (probability of success and the update after a search that
finds nothing), datumpmap() (point, line and area datums on a grid), optcrcsrch() and
opteffort() (the optimal effort for a circular datum and over cells), and optsqr() (the
square a point-datum search should cover, for one search or a series). The numbers agree with
rmorie's to 1e-12. Each result prints and plots, and the dashboard (rmblapp()) has a "Search and
rescue" tab with them and a picker for the book's figures. srchfig() draws each of the 36 figures of
Frost's The Theory of Search (1996) from these functions with the book's parameters, and a new
vignette, "Search and rescue", walks the book's chapters through them.inst/scripts/gen_barrier.R) writes all
of it, so regenerating the file keeps it.rsvidx() answers Ctrl-C: 3e9 items ran 37.9 s through a SIGINT.stepchg(): when every ordering is enumerated the observed one is among them, and is no
longer counted twice (p = 73/721 for 0.1). Drawn permutations keep the +1.trndtst() on a constant series gives p = 1 at every length (it was NA past n = 8).cnttrnd() with zeros at one end only (c(0, 0, 5)) warns that the trend has no finite
estimate and reports a rate ratio of Inf (or 0) with no interval; it was 9.7e16 silently.valrules() on a table of no rows fails no row-wise rule (every rule failed).onlsmry() takes each block's sums in two passes, as var() does: values near 1e8
with a spread of 1 lost nine digits of the variance. The help page says "to rounding", not exact.knoxtst() takes every seed up to 2^53 - 1 (2^40 was refused, -2^31 called too
large), and cpsfals() records a seed past 2^31 - 1 as given (it became NA).[2002:7f00:1::] is 127.0.0.1), Teredo, the local-use NAT64 prefix and the discard prefix; a
307 or 308 redirect repeats a POST as a POST, to the same host only (it became a GET);
wbsave() does not send the public archive a URL on your own network (https://10.0.0.7/,
a .local or one-label name); the manifest is read with a 32 MiB cap (a 300 MiB gzip bomb was
read whole before allocation failed).read.csv(skipNul = TRUE): NUL bytes are dropped before quotes are
read (a quote, a NUL and a quote is a doubled quote; a line of "" with a NUL in it is blank); a
quote after a closed quoted field opens a quoted run again, a comma or line end inside it being text
("x"y",z" is the one field xy,z); a byte-order mark after an empty "" in the first row is
dropped. The two shapes it refuses, where read.csv() changes the data without a word (a record
with more fields than the header, a quote inside an unquoted field), are now in the help page.rmbl "dat list"), a hyphenated one ("xplain-rror?") and a
short line naming a file ("urls.txt?", "my urls.txt") are not questions; a run of 16 or more
letters and digits mixed (an archive.org key) is a key; --model and llmask(model =)
refuse a key given as a model name before any route is looked for (it was sent as the model).svydes(): a post-stratified JK1 set on a post-stratum subset is accepted (the
replicates that delete a PSU with no row there are the weight); the adjustment is read from the
replicates that differ from the weight, and for JK1 from the factors across the set (one factor
n / (n - 1) unadjusted).RMBL_LIVE_TESTS=1. The agentbndl()
example points the settings folder at an empty one, so a saved key cannot make it call a provider.plots.R: the "µg" unit is a value, not a name in c(), so a non-UTF-8 locale keeps it.yoy_*() writer's
digits, stepchg(), scanadj() and the plots' col =; each is refused in words, naming the
argument (the final audit's wider battery, 15 values in every argument of every export, found
90 more raw errors in 31 functions; those are fixed below). Shared checks do most of it: a single
number in a range (.rmbl_scalar()), an environment refused where data goes, a date refused as a
count (reps = Sys.Date() ran 20,000 replications), colours checked before a device sees them (one
check for every plot), titles checked once. morani(n_perm = 1) no longer fails on the spread of
one permutation.chwkres() and the fits built a grid of one point a time unit, 32 GB for
a horizon of 2^31 (the process was killed); a constant baseline needs none, a sinusoidal one keeps a
point a day and refuses a horizon past 4,194,304 days. mksyncsv() refuses more than
100 million rows (it builds the table in memory; 2^31 was a process killed). svydes()
computes its row keys and their digests in C and its degrees of freedom a block of rows at a time:
5e6 rows and 20 replicates peak at 2.8 GiB as a matrix and 2.4 GiB by name (3.5 and 3.1), the design
in 20 s and an estimate in 5 s (52 s each in the stress test).cpsfals(), cpspwr() and the
exposure-response bootstrap caught setTimeLimit()'s error as one more failed statistic and ran on.Rd.sty lets inline
code neither break nor stretch, so the help text was reworded (URL templates split at a slash,
references with the link on its own line, options named in prose, long lists as bullets). No
line is 10pt over now; the 61 left are under 10pt, most under 2pt.mtext(), sized to the row (down to 0.45), instead of axis(),
which leaves out labels that would overlap: trchlthgap()'s 44 rows showed 22 labels beside
the wrong points. plot.rmbl_trc_gaps() adds a key for the groups.cnsprof() matched the profile's column names against UTF-8 keys, so "MÉTIS"
never became "Métis", and its table of column names was built with a non-ASCII name inside
c(), which a package installed in a C locale stores as M<U+00C9>TIS; .rmbl_json_parse() converted before it checked, so invalid UTF-8 was
reported as an unexpected character; the CSV reader's byte-order-mark cases are compared with
read.csv() only in a UTF-8 locale, where its behaviour is defined.rmoriebricklayer.allowhttp was read by the R-level URL check but the native transport still
read the old rmoriebricklayer.allow_http, so bldl() refused plain http whatever the option
said. Three more from the coverage pass: evstudy(leads = 1) failed with "subscript out of
bounds" (no lead left to test); baltab() failed on a group of one unit (its tests are NA
now, as after mtchdid() with a tight caliper); chwkfit(meth = "em") with every event at the
horizon divided 0 by 0 (the branching ratio goes to its floor). mcartst() refuses an infinite
value in its own words rather than as collinearity.DESCRIPTION, the README,
SECURITY.md and the help pages.print.rmbl_yoy() printed "no
periods" for a column subset; the cpsrpt() example read a mrkroot field that is called
merkle_root; the recrd() comments say when a status is INFO instead of PASS; the
onlsmry(), benfordtst(), wrmnfjs(), cpspwr() and plnpois() examples show what
they claim.vignette("plotting-results") gains the policy-evaluation plots, one call per result;
vignette("policy-evaluation") draws the synthetic difference in differences and
vignette("causal-effects") the AIPW estimate beside its overlap.The final pre-CRAN audit (three reviewers and a re-run of every earlier finding)
R CMD check on a host without IPv6 sockets (a kernel booted with
ipv6.disable=1): the socket guard's probe now answers NA where the address family cannot be
opened, and the test skips there instead of reading it as a refusal.read.csv()'s field count is
checked record by record first, and the file is refused in the same words as the in-memory path.round(-0.3) is -0) gave its row an NA key, which wiped the
other columns' contribution and hid reorders among such rows: -0 codes as 0 and every NaN
payload, NA among them, as one value. A design saved by 0.5.14 with NA in a double column
is rebuilt on its data.B x B cross-product: with
more replicates than rows that asked for 74.5 GB (n = 20, B = 1e5) and took 113 s at
B = 5,000. The smaller of the two Gram matrices is read, the replicates scaled first (a
replicate of 1e308 overflowed eigen()).svydes() reads each replicate column against the weight in one native pass
with nothing allocated as long as the column (the R pass held 340 MB a column at 5e6 rows), and
its row-key digests read a column as a set through an order-independent hash in place of a
sorted copy (a radix sort asks for working memory as long as the column outside R's heap, and
that request failed at the limit). 5e6 rows and 20 replicates now fit a 3 GB address space as a
matrix and by name, the matrix design in 6 s (17 before). A domain estimate centres its
linearized values within strata one column at a time and reads the rows of each domain once:
fifty domains at a million rows peak at 0.65 GB of heap (1.9 before).k/(k - 1) on every row
with no row left out, carried by more or fewer replicates than its k (w * 1.005 beside 200
JKn replicates, the full data plus w * 15/14 under JK1), is said as a rescaled copy. Two or more
JKn replicates equal to the weight stay as they are: a stratum left out of a subset leaves all of
its replicates at the weight, and two copies of it cannot be told from that.Content-Encoding: gzip was stored as sent and decompressed whole by readLines(): 300 MiB of
zeros in a 300 KB body (a 2.3 GB allocation, or an OOM kill). The manifest is read a megabyte at
a time through gzcon() and refused past 32 MiB decoded..Call refuses bytes that are not UTF-8 rather than handing back an invalid string, in a C
locale too (R's translation there rewrote such bytes as <ff> before the check saw them).? no longer makes a question of a near-miss command
(dat list?, conifg set openai.api_key?); zero-width characters are removed before the key
check, so a key broken by them is still a key; dotted or underscored runs with digits in two
segments or more, sixteen letters that change case four times or more, and a key typed in groups
of 8 to 16 characters are keys; a UUID, dashed groups of mixed-case characters, a 15-character
mixed token or a key of a known prefix given as --model is not sent as the model name.col = is checked by the field, density, Knox and leave-one-out plots (raw image() errors
before; the leave-one-out plot ignored it).onlsmry() refuses an infinite value in words (smrystat() failed in R's); stepchg() reads a
series near the double's range in units of its largest value (the statistic overflowed to Inf,
the p-value to NaN); fylbl(2024.5) is refused instead of labelled "2023/24".rmbl exits 130 for a Ctrl-C that lands while R is still starting (status 1 and "Execution
halted" before); an interrupt a native call took and a tryCatch(error = ) swallowed as
"interrupted" is raised again as the interrupt it was (rmbl data pull went on for 65 s once in
ten runs).syndid() and synctl() solve their simplex-constrained least squares by an active set on
the KKT system (one small solve a step) with the projected-gradient method as the fallback: the
tiny ridge of the time weights ran the latter to its iteration cap, a second a placebo and 55 s
of CPU in each of two vignettes; 0.04 s now, at the exact optimum (the earlier weights were the
cap's approximation, so estimates move in the third digit).NULL seed (set.seed(NULL) in seven functions), a learner function
of the wrong arity, NULL column names, 1e308 in the air-dispersion arguments, block sizes,
pubbnd()'s suplim and supmar, mksyncol()'s basep, tojs()'s digits, the bundle,
capsule and template file arguments, yoylbl()'s labels, profshr()'s num and den,
plotevgrid()'s main, plnpois()'s offset, trchlthgap()'s tables, wcb()'s trtd,
chgenv()'s per, the strict and ex flags, the col of the rule*() builders and
chwknll()'s par, and shrs()'s total. Each is refused in words now. The re-check of the
fixes replayed the battery and found 29 more in nine functions, refused in words too: blcli()'s
out, cnscwb()'s groups, dlogin()'s token, rtcalc()'s infinite counts, recrd()'s
manifest, plotfnl()'s target and stkflow()'s per. A key typed in groups with punctuation on
its last group ("... qrst9012uvwx?") is a key.read.csv() keeps the mark in the name, X.s): 37 of 24,000 fuzz inputs, documented.Fixes from the review of the native code
siudate() returned "" for every accented French date ("3 août 2017",
"3 DÉCEMBRE 2017") because the native text code folded case with std::tolower(),
which on macOS in a UTF-8 locale rewrites the bytes of a multibyte character as if they
were Latin-1. Case folding of text that may hold UTF-8 is ASCII-only now, in the SIU
parser, the SIU resolver and the transport (r-universe macOS checks of 1.0.0).fetchvrf() no longer passes on R's "cannot rename file" warning when it falls back to
copying the kept file into place.r or s longer than the curve and a key coordinate that is
not exactly one field element: r with 2^1152 added was read back as r and verified.https://example.com./) is fetched as example.com, so
the address pin and the URL libcurl is given name the same host; before, libcurl found no pin
and resolved the name again itself.http_proxy or https_proxy: a proxy on the loopback interface or a private network is
now refused.Inf, Infinity or NaN (any case), so "Info" and
"nanny" are NA, as as.numeric() reads them.cgamcdf() is accurate for large shapes. At x = a it gave 0.4786 for a = 1e4 and 0.0797
for a = 1e6, where the answer is 0.5013 and 0.5001.~ are expanded. A destination that cannot be
written is reported as a local failure, without a Wayback lookup.pubseed, skseed and skprf, as the help pages always said.
Keys saved with the earlier pub_seed, sk_seed and sk_prf still sign and verify.hor.kemsz() and hqcsz() name their sizes public, secret and shared, as the keys and
capsules name them. siusch() names its column count, and fipssz() names the
per-signature randomness rnd.person and addl for the DRBG, pass for PBKDF2,
rnd for SLH-DSA.siuso() removes the glossary sentence in one linear pass. 8000 repeats took 28 s before.siutxt() no longer rescans to the end of the page for every unclosed script or style tag.siudate() refuses an impossible ISO date such as 2023-02-30 or 2023-13-45, as it already
refused February 30.&lt; stays the literal <.tolower().LinkingTo packages have the guards of their .Call versions.
Theil-Sen takes at most 20,000 points and polls for a Ctrl-C, and an exception in any of them
is an R error instead of ending the session.nperm before it allocates, and polls for a Ctrl-C
between permutations.eps or a sum-of-exponentials window that is not one. eps
of NaN silently dropped the excitation term, and 0 ran out of memory.chwknll().NaN are defined: the comparator orders NaN last."wbx").skipNul the NUL bytes are removed before the
scan, so those branches could never run.init.c name the right source files,
and SECURITY.md names RtlGenRandom./dev/urandom is read through a descriptor opened close-on-exec instead of buffered stdio.rmbl drift and rmbl refresh (cpschk(), cpsrfrsh()) fetch a capsule's source over
https:// only. A local path or a file:// URL is read only when it resolves, links followed,
to a file inside the capsule, inside the allowfile folder (TRUE: the folder checked), or in
the package's extdata; never under /proc, /sys or /dev. A source such as
/proc/self/environ or a creds.csv outside the capsule used to be printed in the report.resource$filename of
../../outside.csv was read and its header printed).data_provenance.json that links out of the capsule.rmoriebricklayer.maxcsvbytes when
drift or refresh reads it; a small gzip of zeros ran for minutes.--public-key "" (or any capsule option given an empty value) is a usage error, and
cpsbndlvrf() and cpschkatst() refuse keyexp = ""; both checked against no key.fromjs() reads a \u escape only with four hex digits: \u 041 was read as "A" and
\u-041 as the key "NA".abcd1234efgh;ijkl5678mnop;...)
is a key and is not sent to a model; so is an AWS access key id written in letters only.RMBL_KEY is removed from the process before anything else, and its startup copy is wiped.wbsave() refuses a public name that resolves to a private address.fromjs() decodes a latin1 string in a C locale, and simplifies sparse records about twice
as fast.fdl() passes a time limit on instead of trying the fallback; drift tries the Wayback copy the
provenance pins and no other, and deletes its download.rmbl login and
bricklayer_llm_login() show the terms summary and their address, ask Do you accept ...? [y/N],
and then ask for your name, organisation (optional) and why you need the data; for the restricted
tables (Schedule B: ICPSR, NCVS 38604, A2AJ case law) they also ask for a supervisor, department
chair, director or coordinator and their email. In a script, give them as arguments
(accept_terms = TRUE, name, affiliation, purpose, restricted, supervisor_name,
supervisor_role, supervisor_email) or flags (--accept-terms, --name, --affiliation,
--reason, --restricted, --supervisor-name, --supervisor-role, --supervisor-email).
Nothing is sent until every required answer is there; the data service checks the same things.
login --token with a key you already hold is unchanged.setTimeLimit() during a POST (ask, Wayback /save, the data-key check) is an ordinary
error that tryCatch() catches, said as R says it ("reached elapsed time limit"); it halted
Rscript as an interrupt. A Ctrl-C is still an interrupt.rmbl "cnfig set openai.key" and a
tab-joined $'cnfig\tset\topenai.key' were sent to the first provider; they are a near miss now,
named in "did you mean rmbl config", with exit status 2 and nothing sent.survey_design(): a JK1 replicate on a domain subset that scales every row by
k / (k - 1) (its deleted PSU outside the subset; within 1% of the weight from 101 PSUs) is no
longer refused as a copy; in a post-stratified or calibrated set, replicates equal or close to the
weight are accepted and not warned about (bootstrap and BRR subsets were). A single JKn replicate
equal to the weight is refused as the copy it is, and a JK1 or JKn replicate that leaves no row
out without being such a rescale is warned about, as the Rd said.sk-, gsk_, hf_, AIza, xai-, ...) with twelve key characters
or more is a key however short: a 23-character sk- key in a question went to a model.survey_design(): a column of data passed in a partly named replicate matrix is refused as
in a named one (it was taken as a replicate); two outcomes permuted for a permutation test beside
columns still in place are no reorder (they were refused as one). Replicates given by name are no
longer digested for the row check (the design keeps them): a design by name and each estimate on
it were 2 to 5 times slower than the same replicates as a matrix (10.8 s and 7.8 s for 4.7 s and
1.5 s at a million rows). The adjustment check reads 4,096 rows of each replicate.read.csv(): a byte-order mark at the start of the first row (after
blank lines, or inside its opening quote) is dropped, as scan() drops it; a header of blanks or
of the mark alone is a header of no columns, its rows read as row names.NA, a vector, a function or a number where a path goes), 696 cases in a battery of every
export seeded from its examples; each is now an error that names the argument. Numeric arguments
refuse lists, data frames, factors and complex numbers in words; the writers (yoy_csv() and the
other yoy_*() writers, write_recode_manifest(), write_summary_txt(),
use_capsule_template(), make_synthetic_csv(), bricklayer_download()) refuse a path that is
not one string (a number was written as a file named 1); errors raised in the compiled code no
longer carry the .Call().bricklayer_data_load("csus_2023/pumf") and ("csus_2023/bootstrap"), now hosted on
data.rmorie.com, or downloads them from Health Canada with bricklayer_download(); it no longer
calls rmorie, which this package never depends on.plr_crossfit()
recovers a known effect on simulated data and, on the NSW job-training experiment (LaLonde 1986;
Dehejia and Wahba 1999, hosted as nsw_dw/* on data.rmorie.com), moves the observational
estimate against the PSID controls from -15,205 to 618 dollars, against the experimental 1,794.KEY?, "my key is KEY, thanks?", a key after a
no-break or zero-width space), not only as a whole word: those went to the first provider, the
wrong one for the key. Function names (bricklayer_json_base64url_dec) are not taken for keys. A
verb typo with a ? apart (rmbl "doctr ?"), a leading ideographic space or quotes is a near
miss, named in "did you mean"; a CJK question ending in a full-width question mark is asked.survey_design(): genuine replicates passed by name are no longer refused when two of them
are identical (216 of 2,400 test designs); JKn replicates of other strata, which equal the weight
on a stratum subset, are accepted (every replicate equal to the weight is refused); a stratified
bootstrap whose replicates equal the weight by chance, and post-stratified or calibrated
jackknife subsets, are no longer warned about; a replicate a tenth of the weight's scale (a
normalised weight, a multiplier column) is said, as one ten times it was. Rows reordered and then
a column dropped or one cell edited are refused (the SE was silently wrong); a row-name reset is
accepted. The Rd says how to read the survey package's own bootstrap replicates.setTimeLimit(), not only when
the wait label shows (Wayback /save, OCSP, the data-key check, ask with stderr redirected);
a Ctrl-C in bricklayer_fetch() or wayback_snapshot_url_native() is raised at once instead of
running the Wayback lookups on to their timeouts; the Wayback back-off without a terminal sleeps
a second at a time, so a time limit ends it.bricklayer_data_load() applies the option rmoriebricklayer.maxcsvbytes (1 GiB by
default) on both of its read paths.read.csv(): a line of NUL bytes is skipped; integers with leading
zeros past ten characters are integers; a header of "" alone is a table of no columns; blanks
outside a quoted header name are dropped; quotes after a closing quote are read as quotes; a header
past a million fields is refused before it is held.sen_slopes() leaked 858 MB at a failed allocation; small
leaks in the HTTP header, gini, merkle, XMSS and BLAKE2b paths); a NaN or huge timeout is no
longer cast to an integer (undefined behaviour).hurwitz_zeta(s, 1/2) is (2^s - 1) zeta(s), exact beside the trivial zeros; a
point at -1e300 no longer collapses the Knox and hot-spot grids.set.seed()'s 32 bits the
Mersenne-Twister state is built from the seed (each seed its own stream; seeds in the integer
range give the stream they always did); lagrangian_particles(normals =) must be a function returning n finite numbers; a plot
method given something its class's function did not make says so.<p>AAAA... took four minutes; now
2.4 s).Rplots.pdf into the tests directory; a complex-NA test follows R's own coercion (it failed on
R 4.3); the heaviest signature-vector blocks (about 100 s) are skipped on CRAN and run in CI.bricklayer_progress(label, total), exported: the animated bar of bricklayer_download()
for any loop, as an object with $update(done, info), $tick(by, info), $wait(seconds) and
$done(summary). It follows the download bar's rule (drawn while a person watches a terminal:
an interactive session, or a command line that sets options(morie.progress = TRUE); a line at
each tenth when stderr is not a terminal; nothing with options(morie.quiet = TRUE) or
quiet = TRUE). Nothing is drawn for the first 0.75 s, so a fast call never flashes a bar; a
bar inside another function's running bar stays silent, so nested work shows the outer loop; an
update is a stored value when quiet and a throttled frame when drawing. With total = 0 it is a
spinner with the time so far and the current stage. on.exit(p$done(), add = TRUE) clears the
line when the work fails or is interrupted.exposure_response_plr(), the folds
of plr_crossfit(), the permutations of knox_test() and step_change(), the controls of
capsule_falsify(), the repetitions of capsule_power(), the splits of drift_calibrate(),
the columns of capsule_drift(), profile_columns(), correlation_table() and
top_correlations(), the stages of mahalanobis_outliers(), the EM iterations of
mcar_test() and of core_hawkes_fit(method = "em"), the replicate checks of
survey_design(), the steps of lagrangian_particles() and advection_diffusion_2d(), the
receptors of gaussian_plume(), gaussian_puff() and plume_field(), the files of
capsule_bundle() and capsule_bundle_verify(), the columns and the writing of
make_synthetic_csv(), the packages of capture_dependencies(), the groups and exact p-values
of analyse_table() and yoy_pvalues(), and long vectors in benford_test() and
distinct_sketch() (read in blocks of a million; the counts and registers are those of one
pass).pqc_keygen(), a few
seconds at the default height, about a minute at 14), derive_key(), the native routes of
core_hawkes_fit() (with the stage: first start, second start) and every network request
(bricklayer_fetch(), the portal resolvers, wayback_snapshot_url_native(), revocation and
service lookups, model lists) show a turning spinner with the label and the seconds so far,
drawn by the compiled layer each time the call polls for a Ctrl-C, from 0.75 s on, on a
terminal only. The XMSS tree now polls every 32 leaves (it was every 1,024, once in a whole
default key).isatty(stderr()) alone,
which is FALSE in the RStudio and Positron consoles, so most R users saw only the line at
each tenth. Bars, spinners and the native spinner now animate in a terminal, the RStudio
console (and its build and render panes), the Positron console, R.app and RKWard. They draw
on stdout in the RStudio and Positron consoles, which print stderr in their error colour,
and on stderr elsewhere. Colours follow the console (24-bit, 256, 8, or none; NO_COLOR,
R_CLI_NUM_COLORS, RSTUDIO_CONSOLE_COLOR), and so does the width. They never animate
inside knitr, an R Markdown notebook chunk or a Jupyter kernel, which get the plain lines.
options(rmoriebricklayer.dynamic, rmoriebricklayer.numcol, rmoriebricklayer.unicode, rmoriebricklayer.width), cli's options and R_CLI_DYNAMIC override the detection. The bar
shows the percent, the count (or the bytes), the rate, the time so far and, once the rate
has settled, the time left; it fits the console's width and cuts the label or stage with an
ellipsis. When the work ends, a green check, the label, the result and the time replace the
bar; a red cross replaces it when the function that made the bar failed or was interrupted.
bricklayer_download() shows the same cross. The native spinner of long compiled calls draws
the same line, turns only on R's main thread, ends with a check or a cross, and in a log
writes a line at 0.75 s, one every ten seconds after it and one at the end.New Merkle trees and chain entries are hashed under scheme 2 and record it; anything sealed by 0.5.12 or earlier (no scheme recorded) still verifies under its old rules, with a warning that it was sealed with the weaker scheme.
merkle_root(), merkle_leaves() and merkle_proof() take scheme (2 by default; 1
recomputes a root pinned by an earlier version), merkle_proof() records it in the proof, and
merkle_verify() reads it from there or takes scheme = 2 to refuse a proof without one.
Roots therefore differ from those of 0.5.12 for the same chunks. capsule_report() records
merkle_scheme beside merkle_root.chain_append() records scheme = 2 and links
"v2|<prev>|<payload>|<label>", so editing a label breaks the chain; under scheme 1 it went
undetected.serialize(version = 2) with the four header bytes naming the writer's R version zeroed, so
the same entry gives the same payload on every R version and for any R object.core_bootstrap_mean() draws its resamples with Lemire's unbiased bounded draw on
mt19937_64, as the reservoir sampler already does, instead of std::uniform_int_distribution,
whose algorithm differs between libstdc++ and libc++: the same seed now gives the same resamples
on Linux, Windows and macOS (the Linux and Windows results are unchanged). The means are summed
in long double, so they still agree only to rounding between x86 and arm64.
Every conf_level goes through one check, with one message: trend_test(), count_trend(),
analyse_table() and survey_compare() returned NaN intervals for a level outside (0, 1);
case_flow(), life_table(), disposition_mix(), clearance_decomposition(), rate(),
share(), rate_change(), yoy(), stay_summary() and survey_estimate() checked it in
four different wordings. The interval helpers behind them refuse it too.
manifest_recompute() reports a statistic that returns NA, NaN or Inf as DIFFER
(or MATCH when the record holds the identical value) instead of failing with "missing value
where TRUE/FALSE needed". record() refuses a vector observed or expected in words; it
failed with R's "'length = 2' in coercion to 'logical(1)'".
stock_flow() refuses a t that is neither one value nor one per period (it was recycled in
silence) and a period of the wrong length; adp(), alos(), admissions(),
adp_from_counts() and stock_flow()'s per refuse several values where one is meant (only
the first was used).
profile_columns() leaves infinite values out of the mean, spread, range, quantiles and
outlier count; they are still counted in n_infinite.
core_hawkes_fit() with the exponential kernel also runs from a second start and keeps the
better optimum: from one start it could stop with the decay on its bound at a worse likelihood
and still report converged = TRUE.
reservoir_indices() and reservoir_sample() draw with Lemire's unbiased method on the raw
64-bit output of std::mt19937_64 instead of std::uniform_int_distribution, whose algorithm
differs between C++ libraries: a seed now gives the same sample on Linux, macOS and Windows,
and samples differ from those of 0.5.12. A seed that is NA, negative or fractional, and a
fractional n or k, are refused (an NA seed was undefined behaviour in the cast).
core_bootstrap_mean() refuses an NA, negative or fractional seed and a fractional B
(undefined behaviour in the casts), in R and in the compiled entry point; the compiled entry
point of core_uniforms() now checks its seed too (its R function already did).
core_normal_logpdf() refuses a vector mean or sd and a non-positive sd, as
core_normal_pdf() does; core_gamma_cdf(a, Inf) is 1, as pgamma(); core_trimmed_mean()
and core_winsorized_mean() refuse a trim outside [0, 0.5] instead of returning NaN.
analyse_table() warns that rounding and suppression_limit apply only to counts when
units is not "count"; they were ignored in silence.
Errors about a provenance list name load_provenance(); they named a read_provenance()
that does not exist.
capture_dependencies() gives NA, not "", as the library of a package that is not
installed, and the library of an installed one is the directory the package is in (it was its
parent).
print() of a hot-spot persistence result names each period with its own count of tied
cells ("period 2: 100 cells tied"); periods and counts were pasted into one list.
install_cli() refuses an empty or malformed name; an empty one printed "Installed ;" and
failed with "subscript out of bounds".
yoy_markdown() right-aligns every numeric column; the value and previous value were left
aligned.
With direction = "neutral", print(), yoy_html(), yoy_pdf() and plot() of a change
table draw rises and falls in the neutral colour; print(), yoy_html() and yoy_pdf()
painted a rise in the bad colour, and plot() in the good one.
A malformed certificate, CRL, OCSP response or time-stamp token is an error without the call
(cert_parse() errors read "Error in cert_parse(...)").
Rd: [0, 1] in four help pages was read by roxygen as a link (an R CMD check warning for
hotspot_persistence).
The "is thinking" line of a language-model call is drawn where the console says, on stdout in the RStudio and Positron consoles; it went to stderr, which they show in the error colour.
The 0.5.11 stress round (commit a5a4d10) and the review of this release's own branch (head 6e80794, 2026-10-12): the findings of both fixed, each fix applied to its siblings, and the claims the earlier fixes overstated corrected.
Any major provider, or your own model. bricklayer_llm_ask() takes, in order, an endpoint
of your own, then the first provider whose key is set, then Ollama. Sixteen providers are
built in (bricklayer_llm_providers()): OpenAI, Anthropic, Google Gemini, OpenRouter, Groq,
Together AI, Fireworks AI, DeepSeek, Mistral, xAI, Cerebras, Perplexity, Hugging Face,
OpenCode Zen, Cohere and NVIDIA, each from its usual key variable or a key saved privately
with rmbl config set <provider>.key. Anthropic's Messages API and Gemini's
generateContent are spoken natively, tool calls included; the others are
OpenAI-compatible. route = "anthropic" (or any provider id) takes one route only;
bricklayer_llm_models(route) and rmbl models --route list a route's models;
rmbl config setup asks for a provider, its key and its model.
The model knows the packages. With docs = TRUE (the default) the model is given the
name, arguments and title of every function of the installed MORIE packages
(rmoriebricklayer, and rmoriedata and rmorie when present), read from their installed help,
and five read-only tools: a search over every help page, a help page, its examples, the
dataset list and a vignette. A model that does not take tools gets the matching pages in its
prompt. How much goes into the prompt follows the model's context: everything for a
provider's model or an Ollama cloud model, the functions nearest the question for a model
Ollama runs on this machine; MORIE_LLM_CONTEXT overrides. agent_bundle() asks the same
way (its backend argument is now route).
bricklayer_llm_route_info() says which route would answer, without sending anything;
bricklayer_llm_chat() sends a whole conversation as it is (no help, no tools), for chat
loops in the other MORIE packages. rmorie's help, thousands of pages, is searched by the
tools, and only its functions nearest the question are listed for the model.
The hosted MORIE tier is retired. No route, setting or model list for it remains;
route = "hosted" says so, and a saved hosted route falls back to the automatic order
with a one-time message. Signing in (rmbl login) is for the curated tables at
data.rmorie.com only, and rmbl doctor reports the data key.
A model the route does not have is named as such, with the number it has; a refused key is said as such, and the key never appears in an error.
The data key is checked and issued by the data service. login --token asks
data.rmorie.com (/v1/whoami) whether it knows the key before storing it; the sign-in address
is the services document's data.auth_url, then its llm.auth_url for documents signed earlier.
A document may now leave out the llm block. A restricted table (the ICPSR studies, NCVS 38604,
the a2aj case law) refused with 403 says to accept the terms at https://data.rmorie.com/terms.
API calls no longer end a reply that is slow to start: a model that thinks before answering is not cut off by the 30-second low-speed limit.
It shows that it is working. While a model thinks, the terminal shows a turning spinner,
"options(morie.quiet = TRUE)); Ctrl-C now ends a long wait for a model.
A repeated question is answered at once. The answer to the same question (spacing
aside; case counts), on the same route, model, packages and versions, is kept on this
machine for 30 days and comes back without asking the model: rmbl ask says so, --fresh
(R: fresh = TRUE) asks again. Owner-only files under tools::R_user_dir(), at most 200,
outdated ones removed on each save, the first save in a session says where; only for a
person at a terminal (the command line with the terminal on stdin and stdout) or an
interactive session, never from a script, a pipeline or a check; MORIE_LLM_CACHE=off
turns it off. Nothing is hosted.
Servers can reuse the instructions. The instructions a model gets are the same for every question (the functions picked for a question travel with the question), so the prompt caches of Ollama, OpenAI, DeepSeek and Gemini apply, and long instructions are marked for Anthropic's cache: a faster, cheaper first answer on every route.
Better retrieval. The help search matches inflected words ("emissions" finds "emission", "testing" finds "test") and leaves out question words; the closest help pages go with every question, to large models too; and the model is told never to say the packages lack something before it has searched under other words.
bricklayer_explain_error() and rmbl explain-error "MESSAGE": where an error or warning
of rmoriebricklayer, rmoriedata or rmorie comes from, without a model or the network. Every
message is read from the packages' own code (stop(), warning(), usage errors; sprintf() and
paste() parts as wildcards), matched exactly or by its words, and traced to the exported
functions that reach it, whose help pages say what they expect. With no argument, the last
error of the session. rmorie's 18,000 messages are read once per installed version and kept
beside the saved answers.
Ollama has to be running. rmbl doctor and the help now say to start it (ollama serve,
the Ollama app, or its system service) when nothing answers at its address.
?), it is not a verb or one or two letters from one, it holds no path, line break or
key-shaped word, and anything after it is an option ask takes (rmbl "how?" --fresh works).
rmbl explain-eror "object 'x' not found", rmbl "My Documents/urls.txt" and rmbl conifg set anthropic.key "$(cat key.txt)" with a CRLF key file went to the model before, the key with
them. An unknown verb now names the nearest one (did you mean ...); one that looks like a key
is not printed back.rmbl models exited 0 with "did not answer", a Wayback run
polled on past its time limit): the transfer's callback records the interrupt it takes. The
plain GET behind the Wayback lookups takes Ctrl-C too.stty and sh on PATH without a child process (only /bin and
/usr/bin were looked in, so on a NixOS-style layout the key was echoed), runs the sh it
found, and takes a Ctrl-C already pending before the read starts. config setup prints its
key explanation as part of the prompt the shell shows once echo is off.config set writes only the settings it changes into llm.json (a full rewrite cut
floats to 4 digits, unboxed arrays, renamed a "" key and rounded integers past 2^53).
Answers are kept only with a person at a terminal or in a GUI session (R --interactive < file
kept them). The note after the tool-call cap goes with the last tool result (Anthropic and
Gemini got two user turns in a row).read.csv(gzfile()); a 2.6 MB file of blank lines got the process killed for memory), and a
.csv.gz that is plain text is read as such. As read.csv() reads them: unquoted header names
are trimmed (" Operational_Capacity " came out X.Operational_Capacity.), CR and CRLF inside
quotes are newlines, a line holding only "" is blank, blanks before a quote and text after
one are kept. A quote inside an unquoted field (ab"c, where read.csv() swallows the rest of
the file) is refused in words. Deliberate differences from read.csv(): a UTF-8 byte-order mark
is not part of the first name, and extra fields and an unclosed quote are refused. Short rows
are filled, as read.csv() fills them, unless filling makes more cells than the text has bytes
plus ten million (a 2,000-column header over short lines made 4e8 cells from a few megabytes).
A Ctrl-C while a table is inflated or split is looked for every million fields or blank lines
(by rows it waited 53 s on a wide table), the inflating goes in 16 MiB steps on its second pass
too, and either comes back as R's interrupt, not an ordinary error.?survey_design. New: a copy of the weight within 1 percent on every row is refused under JK1,
JKn and BRR; warnings for more replicates equal to the weight than a bootstrap draws (judged
against the m!/m^m chance for the PSUs read from the replicates), a near copy under the
bootstrap or Fay, a JK or BRR replicate that leaves no row out and is no rescaled weight, and
replicates totalling on average under a tenth or over ten times the weight ($repweights
multipliers), and a replicate over ten times the weight on a typical row (api99 among the
bootstrap replicates raised the SE 21% in silence). 360 generated genuine sets: 0 refused, 0 warned; survey's JK1 and bootstrap
subsets of apiclus1: no warning. A second column with the id's name and other values is refused
(it was kept); the weight beside the replicates under its own name is recognised within 1e-12.hurwitz_zeta() with a whole q beside the trivial zeros goes through Riemann's zeta and the
finite sum (5.4e-8 relative at q = 2 near s = -30); 144 points against mpmath at the exact
doubles agree to 3e-15.knox_test() compares incidents too far out for the distance band to index with every
other one directly (one outlier at 1e300, or a band of 1e-300, refused the whole test), and
measures distances in band units. hotspot_persistence(): a period whose hot set a tie
emptied has no overlap to report (it showed 0); the courts vignette says how ties work.NULL, empty or missing column name in plr_crossfit(),
exposure_response_plr(), exposure_concentration_index() and pollution_burden_by_area().
missingness_summary() and duplicate_rows() take data with duplicate column names, which is
the messy data they exist to inspect.RMBL_LIVE_TESTS=1 (online_now(), the LinkingTo
fallback, an invalid.invalid lookup, a route test's status lookup); the tibble reorder test
resets row names as a tibble does (it passed on builds that never compared values).aye, tellme, or just the question. rmbl aye "...", rmbl tellme "..." and
rmbl "how do I weight a survey?" all ask, with every option ask takes. A first word that is
no verb is asked when a word holds a space or the last word ends in ?. A lone unknown word is
still an unknown verb, so a typo is never sent to a model.inst/bin/rmbl,
inst/bin/rmoriebricklayer, and what install_cli() writes) catch an interrupt that gets past
the command's own handler, which exited 1 with "Execution halted" on the release legs of
84840ac. A Ctrl-C in the first tenth of a second, while R itself starts and before the package
loads, is R's own and still exits 1.sh and stty
with Sys.which(), which runs a child process. R ignores Ctrl-C while a child runs, so the
interrupt killed only the child. The prompt then fell back to a read that ignores Ctrl-C, and
the command waited. They are now found on disk, with no child process.analysis.R sources the package's argument checks
(aaa_input_guards.R, lib_helpers.R) before the package files it loads on its own. Those
files call .rmbl_arg() since the match.arg sweep, and the bundle check failed with "could
not find function".https://llm.rmorie.com (the
retired gateway, the same issuer and key store) goes to the signed document's data address
again, instead of asking to sign in anew. Any other address stays bound to itself.bricklayer_llm_ask() and
bricklayer_explain_error() read this package's help and messages by default; before, they
also loaded rmoriedata and rmorie when those were installed. A package built on this one passes
its own (packages =), and its command line sets RMBL_PACKAGES (comma-separated) for the
verbs it hands over, as RMBL_PROG names the command.-Wformat-truncation); two unused Keccak helpers and an unused constant are gone
(clang -Wunused). The tests expect the warnings they provoke: the stability bound, the
exhausted XMSS key and the record switched off.data passed under its own name is a replicate
because it was asked for. What is refused is exact: the full-sample weight itself (for a
jackknife, BRR or Fay design; a bootstrap replicate that draws every PSU once is the weight,
legitimately), the id's values, the row numbers, a column of data passed under another name,
and a name collision with the weight or the id whose values differ. A genuine replicate that
shares the id's name is kept. A reordered tibble with a vector weight is caught: the design
keeps a digest of every column as given and sorted, and the same values in another order are a
reorder whatever the row names say; a column changed afterwards is not.rmbl models and any GET take Ctrl-C; so does a gzip decompression.MORIE_LLM_CACHE as off, 0, false, no,
none or disabled (spaces aside), and the option as those words too. A script run at a
terminal with stdin and stdout on it is a person watching; </dev/null, a pipe or a redirect
is not. Model names from a server and config values print clean. The saved settings
directory is created 0700 whatever the umask.wayback_retry() looks for, so a URL
captured already today is left for tomorrow; the per-run resubmission cap counts only
resubmissions since the last capture that got through, so a slow archive finishes.MORIE_DATA_URL unset; every test that reaches the live internet needs
RMBL_LIVE_TESTS=1.read.csv() types it (plain integers,
decimals and TRUE/FALSE/T/F decided from the bytes, everything else by
type.convert()): the same table, byte for byte of meaning, in a fifth of the time (a
million-row table: 1.3 s where read.csv() took 6.6 s). A record with more fields than the
header and a quote that never closes are refused in words.tied_out says how many, and
printing says so); cell = 0.1 on even counts no longer calls 100 of 100 cells hot.match.arg()'s "'arg' should be one of" named neither); a data frame with an
empty or repeated column name is refused before anything is looked up; drift_psi(bins = )
takes one whole number up to a million; clearance_decomposition(base = ) takes one of the two
values of compare and keeps its type; an estimate without a standard error plots without a
warning.hurwitz_zeta() beside the trivial zeros takes the distance to the nearest integer exactly
before sinpi() (R's sinpi(x) beside x = -1 is sin(pi * x), which loses the digits of a
value of order that distance: up to 8.9% at -2 +/- 1e-15, on glibc as on other libms); the test
compares against mpmath at the exact doubles. A whole q of 2 or more beside the trivial zeros
goes through Riemann's zeta and the finite sum (5.4e-8 relative at q = 2 near s = -30 before). crime_density() keeps every incident on a map that runs past 180 degrees (the
points are in the grid's frame). hotspot_persistence() says when it drops an incident without
a location or period. A planar cell index past 2^53 is refused rather than merged with its
neighbours. A burden at rate 0 or population 0 draws. The age-specific rate message in
pollution_burden() and its siblings no longer says such a rate cannot exist.rmbl ask, one request at the provider), and nothing is retried after one.login --token checks the key at the signed document's address
only, never at MORIE_DATA_URL.rmbl ask in a script
or a pipeline (the command line always turns the progress option on); now it needs the
terminal on stdin and stdout, or an interactive session. The question keeps its case (df$X
is not df$x), the stored question is checked on a hit, and the first save in a session says
where the answers go.R_ExecWithCleanup): an on.exit() handler is
R code an interrupt can cut short before its .Call, and 73 to 95 locks per 5,000 interrupts
stayed held; 5,000 interrupts now leave none. A record whose name starts with another's keeps
its temporary files.replicates = d[paste0("bw", 1:24)]) are accepted again; a genuine replicate under the
weight's name is refused, not dropped; a replicate that sums to less than a fifth or more than
five times the full weights (a row number, an id, survey's multiplier matrix) or is the same
for every row (a constant) is refused. survey_compare(ref = 2023) takes the number as the
label.crime_density() near the antimeridian (F4) draws: grid nodes past 180 degrees keep their
continued values when wrapping would put them out of order. hurwitz_zeta() past the double
range is a signed infinity, not NaN. (This entry first called F5 a reference artefact; it was a
real loss of digits, fixed in the re-test above.) knox_test() drops an incident without a location or time and says so, as its
siblings do; hotspot_persistence() says when it drops (0, 0); max_pairs = TRUE is refused;
the planar cell key writes whole numbers in full."".
A key with a line break is refused (L3) in R and in C, so it never becomes a second request
header. The settings file is written through a fresh temporary name (L5). A conversation
with help-page lookups gets three times one call's timeout (L6). The progress frame takes a
NaN time. An assumption with ok = NA prints as NA. The age-specific rate message no longer
says such a rate cannot exist. The live transport tests need RMBL_LIVE_TESTS=1 exactly, and
the model tests clear a developer's own endpoint and keys (L4).rmbl_progress_spinner(), a public C-callable,
cast t * 10 to an integer, which overflowed past about 9e17 (nanoseconds) and read another
table's string; the frame is now taken in double, so every time maps to a frame. A bar wider
than an int is clamped before the cast (E13).per_minute (default 6; the SPN2 documentation allows an account 7). A slow-down reply, as a
429 or inside an HTTP 200 (error:user-session-limit and the other throttle codes the SPN2
documentation lists), holds every submission back for its Retry-After or 15 s doubling to
10 minutes, and sends the URL again; eight in a row stop the run. The account's daily cap stops
submitting. Status checks that fail back off the same way. max_captures (default 1000) caps
a run; the rest are left for the next. Before, 300 URLs went out in 32 s.RMBL_LIVE_TESTS=1, and never posts to /save.TRUE looped for ever, 123
stopped the run) is a failure; two URLs given one id each keep their entry; a timestamp that is
not one is recorded as missing, and a results file holding one reads back; a reply over 64 kB is
not parsed. An API call takes its answer only from the host it asked: after a redirect to
another host, that host's JSON had been recorded as archive.org's; the transport now refuses
such a redirect for API calls (Save Page Now, model endpoints, OCSP), and reports Retry-After.
An http page is looked up (it is a parameter of the lookup, never fetched); a key file other
users can read is warned about; a URL with a control or bidirectional character is refused, and
printed results show only what the URLs show. A run that is stopped records the captures it had
started, and the next run polls them instead of sending them again; a missing folder for the
results file is refused before anything is sent. wayback_retry() looks failures up one a
second and leaves a daily limit hit today for tomorrow.data is refused by name (the full weight given as a vector and
the id had been averaged in: a standard error of 0.2599 or 0.2704 where it is 0.2836), BRR and
Fay replicates must come in a Hadamard order, and a matrix with a class of its own is read as
the matrix. The full weight named in weights and shipped beside the replicates is dropped by
name, as before.(t_(n-1) / t_df)^2, as the SAS documentation gives it (it was (z / t_df)^2; the same for a
large n).type = "jkn" with rscales (JKn weights passed as JK1 overstated the
standard error by 37%); the replicate design's degrees of freedom are the rank of the replicate
weight matrix less one, as the survey package computes them (twelve BRR half-samples of an
eight-stratum design have 8, not 11), or degf. survey_estimate() checks the data is the
design's rows in the design's order (reordered data gave a wrong answer silently). A design with
no stratum of two PSUs has no variance to report: the standard error is missing and flagged,
not zero; a certainty stratum is not a lonely PSU; the flag for a domain whose weights are all
zero says so. by = character(0), conf_level = NA and a two-element ref are refused in
words.knox_test() drops it, as crime_density() and
hotspot_persistence() do (twenty such rows had made the closest cell 163 observed against 33
expected). The three share one way onto the plane: points across the antimeridian are one place,
and an extent past 2,000 km is said. crime_density() refuses a bandwidth longer than half the
earth's circumference and says when one is ten times the incidents' spread; knox_test() checks
max_pairs; hot spots made by ties are counted (tied_in) and said when they are most of the
active cells.pollution_burden(),
pollution_burden_by_area() and mortality_displaced() take a rate per person-year between
1e-5 and 0.1 (1 to 10,000 per 100,000), rare_outcome = TRUE below and high_rate = TRUE above,
as verify_pollution() now does on its rate per 100,000 (0.5 had been taken as a rate per
person-year: 14,634 deaths from a million people). verify_pollution() passes the switches on
to its stages. pollution_report_text() refuses a list that only looks like a report.data-raw/tps_break_enter_d14_2024.R: 508 occurrences (the Service publishes one row per
offence record; five occurrences have two, kept as offence_records), every one located. The
old file's all-NA row was a subsetting artefact (an NA year in a logical index), and it carried
one 2025 occurrence and missed one of 2024. The vignette's Knox ratio is computed from the data.
The lone 2026 court row is gone. Sources and licences: inst/COPYRIGHTS, and the DESCRIPTION's
Copyright field.hurwitz_zeta() uses the functional equation for Riemann's zeta at
negative s (full relative accuracy beside the trivial zeros) and no longer overflows a Bernoulli
polynomial whose value is finite (hurwitz_zeta(-4, 5e61)). The terminal prompt is printed by
the shell that reads it, once its trap is set and the echo is off, and a read that never
finished is an interrupt (a Ctrl-C in the first milliseconds had exited 2; a key typed at once
was echoed). bricklayer_llm_status() and rmbl models say a key is stored for another address
instead of "not logged in". An empty schema is said to check nothing (and refused by
apply_schema_validation()); only a one-letter slip or a swap in a structural invariant's name is
taken for a misspelling (min_data_cols is a field of its own). Printing keeps the zero-width
joiner and non-joiner, variation selectors and flag tag sequences. drift_calibrate(n = 0) is
refused. The README counts the 8 entry points written in plain C. The exposure CSV reader no
longer runs two regular expressions over the whole text to see whether it is blank.wayback_retry() and rmbl wayback --retry FILE.jsonl. Reads the results file of an
earlier wayback_save() run and retries every URL whose latest result is a failure: each is
first looked up in the Wayback Machine and recorded as existing when a capture is already
there (the daily limit's refusals, pages captured by another run), the rest are captured again
into the same file, and pages their source no longer has (HTTP 404 or 410) are left alone unless
include_gone = TRUE (--include-gone). Returns the whole run as it now stands, with the
retry's counts.mlkem768_fo_select_validity, compares a pool ciphertext against itself and against another,
both made by the same code, so only the match bit differs. Every target shows no evidence of
leakage locally (x86-64, max |t| 2.8) and the positive control is still flagged.hotspot_persistence(). Incidents counted in grid cells per period: the share of
places holding half the incidents (the law of crime concentration), the hot cells (the busiest
top share, ties included) and their share of incidents, the Jaccard overlap of each period's
hot spots with the previous period's, and per cell how many periods it was hot. Plot: the
concentration and persistence over time and a map of the cells by how often they were hot.plot() of a rate takes period =. A rate by group and period draws one panel per group,
the rate as a line over its interval band on a shared scale.survey_design(), survey_estimate(), survey_compare(). Design-based means,
totals, proportions and ratios, overall and by domain, with variances from replicate weights
(Statistics Canada's bootstrap, mean bootstrap, jackknife, BRR, Fay's BRR) or from Taylor
linearization over strata and PSUs with a finite population correction and a choice for
single-PSU strata. Domain estimates keep the whole design. Replicate totals are computed in C
straight from the data frame's columns (CSUS 2023: 36,180 respondents by 1,000 mean bootstrap
weights in about 1.6 s). Proportions default to the modified Wilson interval of SAS PROC
SURVEYFREQ that Health Canada's published CSUS intervals use. Estimates, standard errors and
intervals match the survey package to machine precision for linearized, JK1, bootstrap and Fay
designs, domain estimates and contrasts; CSUS 2023 lifetime cannabis use reproduces Health
Infobase's published percentages from the public-use file. Domains under 30 respondents are
flagged (Health Canada's suppression rule), not withheld. Plots for both.bricklayer_plot_options(panel = ), NA for none), with no box and
softer axes. Intervals are translucent bands with haloed points; lines carry a soft fill to the
axis and a labelled, emphasised last value; bars sit on the panel with a sheen along the
leading edge and their value at the end; the sequential palette runs deeper for fields and
heatmaps. Drift calibration draws each column's false-alarm rate with its exact interval over
the splits (an all-zero result drew an empty frame). Axis titles, row notes and panel keys
shrink and wrap to fit; labels that fit are not cut.?plot-methods draws every class, the courts and crime
plots on the shipped data included, and vignette("plotting-results") gains a courts and crime
section.yoy() and rate_change() report every percent change. A small base no longer withholds
the percent: 2 to 20 reports +900% with its exact rate-ratio interval, and a change of a few
tenths of a percent off a base of 15 is reported as such. min_base now defaults to 0 for every
unit and, when set, only adds a "small base" note; min_count likewise notes without hiding.
Only a zero base has no percent change. Printed, CSV, Markdown and HTML footers say "note" where
they said "percent withheld", and the period-over-period plot draws small-base percentages.compute_footprint() no longer reads 0 g for fast work. A call shorter than the elapsed
clock's resolution had 0 s of wall time and so 0 energy; the duration is now at least the CPU
time spread over the cores. plot() of a footprint shows CPU, memory and total in one unit
chosen to fit (kg to micrograms), and says "Too short to measure" when even CPU time is 0.wayback_save() and rmbl wayback FILE. Save many URLs in the Wayback Machine with
archive.org's Save Page Now 2 API and your account's S3-like keys (IA_S3_ACCESS_KEY and
IA_S3_SECRET_KEY, or ..._FILE naming files that hold them). Several captures run in flight
at once through the package's own checked libcurl transport; a page captured recently is kept
(within), a page captured already today is recorded with that capture, rate limits and
server errors back off and retry, a refused key stops the run, and each result is written to a
JSON Lines file as it lands so an interrupted run carries on where it stopped. Run live with
real keys on the ledger's sources before release.src/rmbl_progress.c renders the bar (a shimmer along the
filled part, a head that fills in eighths and pulses while work goes on, a spinner) in 24-bit
colour or plain, block characters or ASCII; the R side eases it to each new value at up to 12
frames a second, only on a terminal a person is watching, with milestone lines elsewhere and
NO_COLOR honoured. bricklayer_download() and wayback_save() use it, and
rmbl_progress_bar() / rmbl_progress_spinner() are C callables for the MORIE packages that
link to this one.case_flow(), life_table(), disposition_mix(), clearance_decomposition(),
crime_density() and knox_test(), each with print and plot methods. The clearance rate
with exact intervals and months to clear the inventory; the actuarial life table for banded
times (equal to Kaplan-Meier at the band edges without censoring, checked against
survival::survfit()); outcome shares with Goodman's simultaneous intervals and
Cochran-Armitage trends; Kitagawa's decomposition of a change in a clearance rate into rate
and case-mix effects with a Mantel-Haenszel rate ratio and a Simpson's-paradox verdict (on the
Court's own figures, 2014 to 2022: the aggregate fell while the rate at a fixed mix rose);
binned Gaussian kernel density with highest-density regions (checked against
MASS::kde2d()); and the Knox test with exact expected counts (checked against a full
enumeration of permutations).inst/extdata (?justice-data): Ontario Court of Justice monthly flow and
time to disposition (2024-2026), outcomes, offence groups and courts by calendar year
2012-2025, each reconciled to the published provincial totals; Toronto Police Service break
and enter occurrences in division D14, 2024. Vignette courts-and-crime.strwidth() scales by par("cex"), which a panel grid lowers, and mtext() does not, so
the key ran off the device); the plume and particle titles fit their panel.apply_schema_validation() checks for a data frame before the schema.hurwitz_zeta() is accurate below s = 1. The Euler-Maclaurin shift added in the fifth
pass summed |s| + 20 terms and cancelled them against a small result: every digit was lost
below s = -5 (zeta(-11) came back as 5.4, zeta(-15) as -2.3e7, where 52a8883 had returned NA),
and near s = -5 only six digits were right. Now s <= -5 uses Hurwitz's Fourier formula on
(0, 1] (exact zeros at the even negative integers, zeta(-200, 2) = -1 exactly), -5 < s < 1
Hermite's integral on [1, 2), the integer s in (-5, 0] the Bernoulli polynomial, and any other
q is shifted into range by sums that do not cancel. Checked against mpmath at 40-60 digits over
112 points: the largest relative error is 1.9e-13. A tiny q keeps its own term
(hurwitz_zeta(0.5, 1e-20) was Inf). The fifth pass's test compared against a reference
that was wrong in the fifth digit with an absolute tolerance that accepted 0; it is relative now.gzfile() returns what a
cut gzip or bzip2 file decodes to, with no error and no warning, so half a file was read as a
whole one (status ok). gzip is now inflated in C with zlib, every member's CRC-32 and length
checked (several members, as gzip -c a b and data.table::fwrite() write, are read whole);
a bzip2 file must end with its end-of-stream marker; an xz decoder warning is an error. The
decompressed size is capped while it is read (options(rmoriebricklayer.maxcsvbytes),
1 GiB): a few megabytes of gzip no longer grow to gigabytes before anything is refused. zlib is
now a system requirement (it ships with R's own build dependencies).rare_outcome = TRUE, whatever its unit. The
check ran only when baseline_rate_per was missing, so a wrapper or do.call() that always
passes it turned 0.008 per person-year into 0.008 per 100,000 and a burden 100,000 times too
small, with status ok. The check is on the converted rate now, and below 1e-6 per 100,000 no
rate is accepted (baseline_rate_per = 1e300 was).value column with "<2" is refused, as an exposure column is. The value was
converted before the text check and counted as a row with no measurement (prevalence 1.0
instead of 0.4).--force belongs to config reset alone. config set own.key --force stored "--force" as
the key. config show, help, get and unset refuse extra arguments, and arguments after a
bare -- are refused by every verb but ask and bundle (functions -- a b listed everything).file.exists() only, so a FIFO blocked signing for good while the lock was held.{}, an error object, a datasets that is not a list of tables) is refused and the
cached copy kept./bin/sh is dash (Debian, Ubuntu). The shell's trap ran,
but system2() reported no exit status for it, so the interrupt came back as an empty line and
a usage error (exit 2, or 1 at the e-mail code prompt). The trap now also prints a marker R
recognises. The fifth pass's test mocked system2(); this one runs the launcher in a real
terminal (util-linux script), under dash and bash.next_index past 2^31 - 1 is the damaged-key error, not a raw NA comparison error.apply_schema_validation() needs something to validate against: NULL, an empty list or a
provenance without schema (a typo such as scheme) is an error, not TRUE. A schema field a
few letters from a known one (expected_colums, min_data_row) is fatal with a "did you mean";
other unknown fields are said. expected_dtypes, the name the provenance files use, is checked
as expected_types (the OTIS file's column types never were), and a factor matches a character
column.ask with no route says
the key is stored for the other address instead of "no key stored".data list removes every default-ignorable code point (soft hyphen, U+034F, Hangul fillers,
U+180E, the byte-order mark, the interlinear annotation marks, tag characters), not only the
bidirectional and zero-width ones.verify_pollution() refuses a non-numeric scalar such as as.raw(25); the dispersion solvers
refuse n_steps past 2^31 - 1 even with max_work = Inf, and a particle grid larger than
max_work cells; plot() of what is not a pollution report says so.cex = 2 on a 2-3 inch device failed, and a 4-inch plot
cut labels it had room for); the bar for missing values never takes a real level's name.flock() (Unix) or LockFileEx() (Windows) on
xmss_used_indices.txt.flock, in C: the kernel releases the lock when the process ends, so a
crashed session leaves nothing to take over, and the 10 s wait is bounded and draws no random
numbers (lock contention no longer moves set.seed() streams). A read-only directory is said
as such.fsync(), size-checked and only then renamed over the old one; a full disk stops
the signature and leaves the old record in place (it emptied the record before).root field it carries: a
stale copy whose root was upper-cased, padded or swapped is still the same key, and a
signature whose root does not match the seeds is withheld (its index stays retired).xmss_register_key() vouches for it.xmss_index.json next to the record
is read and merged, and kept as xmss_index.json.migrated once the new record is written; an
unreadable one stops signing. Lines of the 0.5.12 pre-release format are read as well./dev/null, a directory, "none", "-" and relative paths are refused.xmss_advance() without message accepts a signature index only up to the highest index the
record shows this key has used here, so an edited index cannot burn the leaves in between; a
bundle is accepted as documented; a damaged next_index is a clear error.
xmss_forget_used_indices(key) takes the signing key (the record is keyed by its seed).
The help says what to do in CI or a container with a fresh home directory.verify_pollution() reads gzip, bzip2 and xz CSVs again (the byte reader of the fourth pass
refused them as "NUL bytes"), decodes UTF-16 without a byte-order mark, and stops on a parse
problem such as an unbalanced quote instead of returning the rows before it (status ok with
mean 12.2 instead of 36). The decoded text is parsed from a file, not a text connection, so a
large file is no longer several times slower, and it is held once rather than with a
file-sized logical vector beside it.baseline_rate_per = 1e5 for a rare outcome, 1 for a rate per person-year); the note now
says which to pass. Exposure cells that are text rather than numbers ("<2") stop the run and
are named, since dropping them biased the mean and the prevalence; empty and missing cells are
dropped and the report prints how many. exposure_csv with demo = TRUE, a population above
ten billion and an empty income column are handled with a reason.credentials.json is validated with the real parser before it is edited, and the edited text
is parsed again before it is written: bare words, missing or trailing commas, trailing
garbage and duplicate keys are refused instead of written into (every later read failed).rmbl config setup reads the MORIE key and your own API key without echo, and the echo is
switched off and on by the shell child itself, so Ctrl-C cannot leave the terminal silent;
Ctrl-C at any prompt exits 130. A key given on the command line (login --token KEY) is
warned about. data list, data pull, functions, config path and config setup refuse
extra arguments. config reset moves a readable file only with --force, never overwrites an
earlier backup, and is listed in the help.hurwitz_zeta() sums a negative q in closed form: no cap, no billion-term sum, and none of
the cancellation that returned 0 for s = 5 at q = -99999.5; below s = 1 it returns the
analytic continuation (hurwitz_zeta(0.5, 0.5) is -0.6049).lagrangian_particles() has the max_work cap of advection_diffusion_2d() (particles x
steps); gaussian_plume() and gaussian_puff() take at most 1,000 lid image pairs; a step
count above 2^31 no longer fails in the report title.(missing) keeps its name, and labels are cut to the device's width.print() of an attestation or bundle written by 0.5.9 to 7703a04 warns that it carries the
signer's private key. fips_verify_mu() returns FALSE for an edited verifier key instead of
an error, and a scheme given as two names is refused. hill_tail_index(x_min = factor(...)),
cramers_v(bias_correct = "abc"), cramers_v(min_expected = NA), plot_funnel() with a bad
levels or target and pollution_report_text() of a non-report are refused with a reason.MORIE_DATA_URL that is not the document's address is named in the error.xmss_used_indices.txt), written through a unique temporary name whose rename
is checked; a malformed line stops signing until the file is repaired or removed; the off
switch warns on every signature; a value that is not a path (TRUE, "0") is refused.
pqc_keygen() registers every key in the record at birth, so a key the record does not
know that claims never to have signed is refused (a deleted record, or a key from another
machine) until xmss_register_key() vouches for it or xmss_advance() carries it forward.
xmss_advance() checks the index against the key's height, verifies the signature when the
message is given, never advances below what the record knows, accepts an attestation, bundle
or recode manifest, and warns when the key is exhausted; xmss_forget_used_indices() can
forget one key and is documented as removing the protection. Examples and vignettes point
the record at a temporary file so checks and builds leave the configuration directory alone.verify_pollution() reads the exposure CSV whole: UTF-16 is decoded, a UTF-8 BOM dropped, a
file that is not UTF-8 read as Latin-1 and said, NUL bytes refused (the fifth pass found a
quote that swallowed the rest of the file still did, and that compressed files were refused). population = NULL or baseline_rate = NULL is an error report with a reason. baseline_rate may be zero (a measured zero);
a value between 0 and 1 per 100,000 fails the assumption and suggests baseline_rate_per = 1, the new argument for a rate quoted per person-year.hurwitz_zeta() refuses q below -1e6 instead of allocating the recurrence's billion terms;
logical q, x_min, fractions and non-logical discrete/approx are refused.advection_diffusion_2d() has one cap for every run, max_work (100 million cell updates,
Inf lifts it): the same physics costs the same whether the caller sub-steps it or the
scheme does, so a run cannot be refused in one spelling and accepted in another.credentials.json is edited as text: login and logout touch only this package's three
members and every other byte (another package's floats, large integers, empty keys,
non-ASCII text) stays as it was.print() and capsule_bundle_read() warn about an old
object that carries key_state; an edited verifier key object is "not verified", not an
error; verify_pollution() refuses scalars beside a CSV or the demo, refuses list and
complex scalars, says how many non-numeric exposure rows it dropped and skips the equity
index when fewer than two rows have an income; bar labels over 28 characters are cut so a
long level cannot make the figure margins too large; own-endpoint settings without an
address are said when the hosted tier is used instead; OLLAMA_BASE_URL in the environment
wins over a saved address; own.key and own.url in one call keep the key; rmbl config reset moves an unreadable settings file aside and doctor reports the file instead of
failing; verbs that take no arguments refuse extra ones and Ctrl-C exits 130; U+061C,
U+2028 and U+2029 are stripped from listings; a schema that is not a list is a clear
error; a stack within half a kelvin of the air has zero rise (no cliff); Anguilla uses
Antigua's grid and Jersey and Guernsey France's; Ukraine's 2022 row is named as such; a
note line is never under a tenth of an inch and one inline note no longer shrinks every
page of a yoy_pdf().q, the cap's consistency, and the credentials bytes.key_state from attestations only; capsule_sign() itself still returned the
advanced key, with sk_seed and sk_prf, inside every XMSS signature, so every shared
signature, bundle JSON file, chain and report made with XMSS from 0.5.9 to 7703a04 handed
out the private key, and anyone holding one could attest or sign anything under the
victim's root. The signature object now carries no key material of any scheme, and the next
key state is xmss_advance(key, signature) (new, exported, with xmss_forget_used_indices()).
Treat every XMSS key whose signature, attestation, bundle or report left your machine
under 0.5.9 to 7703a04 as compromised: generate a new key and sign again. A test checks
that no signature, attestation, bundle, JSON or serialisation of any scheme contains
private-key bytes.xmss_index.json under tools::R_user_dir("rmoriebricklayer", "config") (the directory CRAN
policy sets aside for a package's own configuration), so a key reloaded from disk with a
stale next_index is refused on the same machine, in a fresh session too. The option
rmoriebricklayer.xmssidxfile or RMBL_XMSS_INDEX_FILE moves the file; "" or FALSE
turns it off. The record does not travel with the key: save the advanced key after every
signature, as before.verify_pollution() with a mean but no prevalence (or the reverse) is an error report (exit
2), not "ok, 0 cases": the first pass's zero-exposure fix had made the omitted input a
silent zero. Both scalar defaults are now NULL. A per-100,000 baseline_rate below 0.01
fails an assumption as a per-person rate typed in the wrong box; an exposure above
10,000 ug/m3 fails the range assumption instead of throwing; a UTF-8 BOM, a negative row and
a date are handled; the attributable-fraction step is inside the report contract.advection_diffusion_2d() caps only runs that sub-stepping inflates: a large stable run is
the caller's to wait for. The number in the message is formatted properly and the advice is
correct (a smaller dt lowers the sub-step count). lagrangian_particles() refuses more than
100 million particles; gaussian_puff() warns about receptors below ground or above the lid.plot() of a frequency table with a missing level draws it as "(missing)" instead of failing
on the margin (broken since 0.5.11).capsule_verify() answers FALSE to any edited signature field; capsule_check_attestation()
accepts a public-key object as key_expected (for XMSS the root and seed both) and reports a
missing digest instead of throwing.llm.json
(route: 5, an array, true, "") is an error, not "auto"; a credentials file without
hosted_base_url (morie writes one) sends the key only to the services document's address;
MORIE_DATA_URL receives the stored key only when it is the document's curated-data address;
login and logout keep another package's entries byte for byte (the file is edited as text);
a saved address may not contain whitespace, a fragment or user information.rmbl data list strips the C1 controls and the bidirectional format characters as bytes, in
any locale; the help never shows --token KEY.bricklayer_data_load() refuses a manifest digest that is not a digest (a sha256: prefix is
accepted), and a mismatch refreshes the manifest once before calling the file bad, so a table
rebuilt upstream does not fail until the cached manifest expires.apply_schema_validation() warns when the provenance list has no schema (a typo such as
scheme is no longer silent) and refuses an environment.hurwitz_zeta() handles a non-positive non-integer q (8.93 at q = -0.5 for s = 2) and
refuses a pole; top_share() refuses a fraction outside [0, 1] (0 is a share of nothing); hill_tail_index() refuses a
vector x_min; rapl_available() refuses a non-path; exposure_concentration_index() refuses
an infinite exposure. The masking-cost sentence of the side-channel vignette carries the
measured 17 ms against 2.4 ms. crf_pm25() and crf_no2() refuse a logical exposure.top_share(x, 0) is zero units again and
apply_schema_validation(data, NULL) means nothing to validate (two guards of the second
pass overshot and turned CI red); capsule_verify() checks the shape of a signature before
the type of the key, so an HMAC signature whose scheme was edited to XMSS is FALSE while a
real XMSS signature with a string for a key is still an error; a FIPS signature whose recorded
context or prehash is not a string is FALSE; verify_pollution() with a missing
reference is a report, not a crash; briggs_plume_rise() at ambient temperature has zero
flux and zero rise (a cooler stack is refused); Ctrl-C at the rmbl login --token prompt
interrupts at once (the terminal line is read by a shell child, so the interrupt is not held
until Enter); the yoy_pdf() notes test asserts the page rule instead of comparing a value
with itself; the dead-stack note on the two gadget pairs says what they hold. Verifying an XMSS
signature or attestation that still carries key_state warns that the key is compromised.capsule_attest() embedded the
whole signature object, and an XMSS signature carries key_state (the advanced key, with
sk_seed and sk_prf) so the signer can save it. Anyone given such an attestation could
attest anything under the victim's public key. The key is stripped before the signature is
embedded (as the recode manifest already did), and a test checks no attestation carries a
secret. If you shared an XMSS attestation made by 0.5.11 or earlier, treat that key as
compromised: generate a new one and re-attest. HMAC and ML-DSA attestations were never
affected.verify_pollution(exposure_mean = 0, exposure_prevalence = 1) and (25, 0) are valid
runs with zero attributable cases again; "no data" is now only a call that passed neither.
Every pipeline stage runs inside the status contract (a text income column, all-zero
exposure, one row and a per-100,000 baseline_rate are reports, not raw errors); a factor
argument is refused; the assumption log prints population=1,000,000.pollution_burden() and pollution_burden_by_area()
(800 per 100,000 taken per person-year gave 110 million deaths from a million people), and
the message says which function takes the per-100,000 form. crf_no2() refuses a logical
or factor beta_per_10 (TRUE gave a relative risk of e). n_bootstrap must be whole.apply_schema_validation() refuses a list that mixes rules with anything else, a nested
rule list and a data frame, instead of falling through to the provenance checker and
passing.advection_diffusion_2d() caps the work (cells x steps x sub-steps, 10 million, about five seconds) rather
than the sub-step count: a 5 x 5 grid at 10,001 sub-steps runs in a tenth of a second
and is allowed, a 201 x 201 grid at 9,999 for 100 steps would take hours and is refused.
The cap no longer rounds 10,000 up to 10,001. A negative source and a logical u are
refused; a receptor below ground or above the mixing lid is warned about; the open
boundary is documented.credentials.json
(shared with morie and rmorie) that does not parse is an error and is never written over;
llm.json that is not a JSON object ([], "x", 5) is an error too, not a warning
that fired once and then let a saved route be dropped. The hosted key is tied to the
address it was saved for; a new own.url or ollama.url drops the key saved for the old
server; ollama.url refuses a metadata or link-local address when saved.bricklayer_data_load() checks the manifest's SHA-256 of a cached or downloaded table and
deletes and refuses one that does not match.fips_verify_mu() answers FALSE to a length-2 signature field (the shared hex helper
takes one string); an XMSS signature whose index or height is not a whole number, or
whose root, height, wire or randomizer was removed, is not verified, and a missing
randomiser is FALSE rather than an error.en_US.rmbl data list keeps accents in a C locale (control characters only are stripped);
rmbl ask -- --like-this passes a prompt that starts with --; inst/bin/rmbl is
executable in git; the help and the vignette suggest login --token with the key pasted
at the prompt, never on the command line.yoy_pdf() sizes its notes to the line allowance the pagination made, and many notes go
on pages of their own (30 notes on a 5-inch page lost notes 24 to 30).plot_lorenz() sorts a hand-made frame and refuses one that is not a Lorenz curve; an
all-NA axis no longer breaks the margin computation. lagrangian_particles(n = 2.7) and
compute_footprint(usage = c(...)) are refused.location = "GLOBAL" is an error
since the duplicate row went (use "WORLD"); the rows older than 2024 include Ukraine
(2022).r0/r1,
e10/e11, e20/e21, u0/u1, v0/v1, the FO-check shares ok/msk and the masked
PRF's input and output buffers (whose sum is the coins G(m' || h)[32..64]). Summing the
two halves of r-hat recovers m' from the public key and ciphertext alone, so a memory
disclosure anywhere in the process was a decryption oracle for an attacker-chosen
ciphertext. Every pair the scanner can see is wiped now, with the per-group and per-word
temporaries; two gadget-internal 32-coefficient pairs (sec_in_interval, sec_b2a)
remain on the dead stack (they hold e2 + eta and v' - lo; v' contains Decompress(m'), so
the second pair also encodes 32 bits of m' for a chosen ciphertext: partial leakage, no key
recovery shown), and one dsa_bprime pair remains on
clang -O2. The 0.5.10 note that "every share pair is now wiped" was false and is corrected.v = w - c s2, the high bits of w and of the hint) as it goes; one
chunk of v survived on some builds and revealed part of c t0.s*u, w = v - su, Compress_1, a Keccak permutation over the message shares) and the
ML-DSA z path (y from Boolean shares, z = y + c s1, the norm test, the low-bits test),
so the hand-over between gadgets is measured too (the harness fix that let them run is
above; the tvla workflow reports the value-model and transition-model t-statistics). The
dudect gate on a DIT-capable CPU is the no-evidence line (|t| > 4.5 after the tripled
re-measurement fails), not |t| > 10.r-hat and m' from the message it
encapsulated and scans for both after the masked decapsulation; this is the check that
would have caught the gap above.capsule_verify() answers FALSE, not an error, for a signature whose signature field
is not one string, whose prehash names no digest, or whose XMSS auth, randomizer or
index is missing; and an XMSS signature whose own root, height or wire disagrees
with the key or with its parts no longer verifies (verification used the key's values and
ignored the edited fields).backward alias the
country of the zone it links to, which is wrong whenever the link crosses a border:
Iceland was Ivory Coast (Africa/Abidjan), Atlantic/Jan_Mayen Germany,
America/Coral_Harbour Panama, Africa/Asmera Kenya, Pacific/Truk Papua New Guinea.
The builder now reads the #= annotation of the IANA backward file (the zone the alias
stood for before the merge), takes the target's country only when that target serves a
single country in zone1970.tab, and keeps a reviewed table (tools/timezone_alias_countries.csv)
for the remaining cross-border aliases; the generic zones CET, EET, MET, WET,
EST, HST, MST and the *ST*DT names resolve to no country. Rebuilt from tzdata 2026e.detect_location() warns when RMBL_LOCATION names a location the table does not know
instead of silently moving on to the time zone.compute_footprint() rejects a fractional cores.GLOBAL row (480 gCO2e/kWh from the Green Algorithms file) is gone from
the carbon-intensity table; WORLD (Ember, 458.29) is the world average, and
location = "GLOBAL" is now an error. The documentation
no longer claims every row is from 2024 or 2025.verify_pollution() with no exposure data at all (no CSV, no demo, mean and prevalence
both zero) is an "error" report with exit status 2, not a clean run with zero deaths;
an exposure at or below the reference stays a valid run with zero attributable cases. The
input contract is enforced in the report rather than as raw R errors: a vector where a
single number is expected, a missing or negative reference, an unknown outcome, a
non-logical demo, a directory as exposure_csv. The report prints the population as
1,000,000, not 1e+06. The vignette's failure demo fails again.advection_diffusion_2d() refuses a run that would need more than 10,000 sub-steps per
step (dt = 1e5 on the previous round's reproducer meant 7.8 million sub-steps, with the
warning deferred until the end) and a wind that overflowed the sub-step count to NA;
c0 must be a non-negative numeric matrix (a vector gave a field of mass 0) and source
finite.briggs_plume_rise() rejects temperatures below 150 K, which are Celsius values typed
where kelvin is expected (111 m of rise instead of 51).lagrangian_particles() rejects a grid whose bounds are reversed or whose cell counts
are not whole (it gave a negative concentration).crf_pm25() and crf_no2() take exactly one finite reference_conc (a vector recycled
against the exposures; NA gave NA risks). attributable_fraction() rejects a logical
relative risk. mortality_displaced() rejects a baseline rate above 1 (a per-100,000
figure passed where a per person-year rate is expected gave 23 million deaths from a
million people).plr_crossfit() requires a whole k_folds (2.7 ran with 2) and numeric columns (a factor
outcome was analysed by its level codes).exposure_concentration_index() refuses a factor or character income instead of ranking
level codes, and negative exposures; pollution_burden_by_area() requires numeric
exposure, population and baseline columns. The index's n_quintiles field, a constant
5, is gone, and the documentation says ties share their average rank.hurwitz_zeta() takes one q.attributable_fraction(rr_lower =, rr_upper =) carries the relative risk's confidence limits
through Levin's formula; crf_pm25() and crf_no2() carry the published 95 percent limits
of their risks (rr_lower, rr_upper; NA for the IER forms), and verify_pollution()
reports the fraction's interval.llm.json is an error and no setting is written over it: the next
config set used to rewrite the file with only the new value, deleting the saved keys and
addresses.MORIE_LLM_BASE_URL=... or
OLLAMA_HOST=... set for one call receives the environment's key or none, never the key
saved for another server (the /api/tags probe included).rmbl ask --model x --model y ... is a usage error instead of sending "--model y" in the
prompt; --route auto takes the automatic order over a saved route; an invalid
MORIE_LLM_ROUTE is an error, not silently "auto".config set own.url runs the same address check as use (a metadata or link-local address
is refused when saved); ollama.url must be http(s).stty exists), and the help no
longer suggests putting the key on the command line.rmbl data list strips control characters from the manifest before printing.apply_schema_validation() accepts a rule list and runs it with validate_rules() under
the same stop-on-fatal contract; it used to ignore a rule list in its second argument and
report a pass, which is what the vignette told readers to do.plot_funnel() requires one area label per point. An empty frequency_table() draws an
empty page with a title, and plume_field() requires increasing coordinates, with
messages of their own instead of errors from inside the palette or base graphics.yoy_pdf() paginates on the rows that fit the page height after the title block, the page
label and the notes (.yoy_rows_per_page(), with a test); a 24-inch page planned 98 rows
where 88 fit and dropped the last rows.agent_bundle() help says
which route the request takes.pkgcheck workflow fails when pkgcheck does, and no longer asks for
issues: write.doctor listed every hosted model, could
not reach any of them. An empty Ollama is now passed over unless OLLAMA_MODEL is set or
the Ollama route is asked for.bricklayer_llm_config() and rmbl config show and save every language-model setting:
the route ask takes (auto, own, ollama, hosted) and the address, key and model
of your own server, of Ollama and of the hosted tier. rmbl config setup walks through
them. Settings are saved in ~/.config/morie/llm.json (mode 0600); an environment variable
that is set still wins.rmbl ask --route ROUTE picks the route for one call.rmbl doctor ends with the route and model ask will use, and how to change it.rmbl help is a guide: the verbs by area, and help start, help llm, help config and
help r pages with examples. Error messages name the config command that fixes them.rmbl data list and printing bricklayer_data_tables() in R show one line per table:
the key, the row count and the name, left-aligned and cut to the terminal or console
width. They went through print.data.frame, which right-aligned the names and, once the
descriptions made a line wider than the terminal, printed every key, then every name,
then every row count and source as separate blocks. bricklayer_data_tables() still
returns the full data frame (with source); only its printing changed.inst/smoke/smoke.R) prints a failing case's message in full. It cut
them at 400 characters, which shortened "https://rmorie.com/access" in the no-route
message of ask to "https://rmorie.co".A version of its own. The build tagged 0.5.9 on 2026-10-07 (29ff122) and the one
that followed it the same day (dcb3cb9) shared a version number while differing in the
masking defaults, the FIPS 203 section 7.3 key check, capsule_attest(context = raw) and
the whole air-pollution feature. The stress test of dcb3cb9 (2026-10-08) asked for a
number that tells them apart, and for the fixes below; this is both.
plot() methods for every result the package returns, on base graphics only: stock and
flow (stock_flow(), one panel per measure), period-over-period change with intervals
(yoy()), rates, shares and rate change, band sensitivity, drift by column
(capsule_drift(), drift_calibrate()), Benford digits, power curves, frequencies,
missingness patterns, region coverage, Mahalanobis outliers, correlations, permutation
nulls, rounding envelopes, concentration-response curves, burdens, concentration curves,
the whole verify_pollution() report on one page, the footprint with its equivalents,
concentration fields and particle clouds, count trends, and analyse_table() one panel
at a time. The same customisation everywhere (main, col or palette, grid, ...
to the base call), bricklayer_plot_options() for the session,
bricklayer_palette() with four colour-blind-safe palettes, and every method returns the
data it drew. plume_field() evaluates a plume on a grid; plot_lorenz() and
plot_funnel() serve the results that are plain data frames. vignette("plotting-results").
Titles fit their panel: a title too wide is drawn a step smaller, in a panel set one that
still does not fit becomes a letter with the key printed under the panels (returned as the
panel_key attribute), long axis titles shrink or wrap, and the left margin is sized from
the widest tick label so an axis title never overlaps the numbers.exposure_concentration_index() records its concentration
curve, count_trend() its data, verify_pollution() returns class
rmbl_pollution_report, advection_diffusion_2d() class rmbl_field and
lagrangian_particles() class rmbl_particles (all still plain lists underneath).verify_pollution() applied the PM2.5 counterfactual (5.8) to NO2, overstating NO2
deaths by 27%; reference now defaults per pollutant (NO2 10, PM2.5 5.8), as
pollution_burden() always did.exposure_mean is their mean and exposure_prevalence their
share, and the scalar arguments are documented the same way.advection_diffusion_2d() documented a stability criterion that was false (cfl below 1
and diffusion number below 1); the explicit upwind-plus-FTCS scheme needs
|u dt/dx| + |v dt/dy| + 2 (kx dt/dx^2 + ky dt/dy^2) <= 1, and a run outside it reached
10^10 from a unit pulse while reporting itself stable. The bound is returned as
stability_number and stable; a call outside it is taken in enough sub-steps of a
smaller dt to satisfy it (reported, with a warning), so the field is a solution over the
same physical time rather than an explosion.n_images a whole number (2.5 was truncated silently), the stack
hotter than the air for Briggs' rise (a cooler stack returned NaN), x0 and y0 single
numbers (a vector doubled mean_x), and a warning when the release is above the mixing
height. gaussian_plume(q = -100) returned a negative concentration and u = 0 returned
Inf; gaussian_puff(t = -100) a negative one; pg_sigmas(-500) a negative sigma.attributable_fraction() returned 2 for RR = -1, 0 for RR = 0 and NaN for RR = Inf, and
coerced text; it now requires a single finite positive RR and a prevalence in [0, 1].
pollution_burden(), mortality_displaced() and the CRFs check their inputs the same
way; population is kept as a number (8e9 is no longer NA); "PM2.5", "PM25" and
"pm2.5" are accepted everywhere.value column without a unit column is an
error rather than assumed, and any other unit is an error.pollution_burden() takes a rate per person-year, verify_pollution() per 100,000.XK), documents its provenance and
licences, and says which values Ember reports as exactly 1000 or above it. The time-zone
table is rebuilt from the IANA zone.tab (one country per zone) plus every alias in
backward, 549 zones: Europe/Stockholm, US/Eastern, Asia/Calcutta,
America/Montreal and about 130 other common names were missing and fell through to
the locale, so an English (US) locale in Stockholm got the US grid. Every geographic zone
in OlsonNames() now resolves, and the posix/ and right/ prefixes are stripped.
compute_footprint() checks every argument before it runs expr (a negative
memory_gb no longer gives negative CO2e after the computation has been paid for).capsule_sign(masked = FALSE),
fips_sign_mu(masked = FALSE), and the option rmoriebricklayer.masked sets the
default for all three masked operations in a session (a chain or bundle signs many
times). The documented cost was understated: masking takes about 3 to 12 times the
plain time on decapsulation and 13 to 24 times on ML-DSA signing, depending on the CPU
(4 ms and 26 ms on this package's test machine).rmbl_mlkem_body.h and rmbl_mldsa_body.h,
where the composed masked algorithms live; the dudect runner re-measures every target the
first pass puts above the no-evidence line (|t| > 4.5) with three times the samples before
judging it, because a shared CI runner moves a target by several units of t between passes
(the arm64 runner put the unmasked ML-KEM decapsulation at 6, 9.7 and 10.6 within an hour,
for seven ticks in eighty-four thousand; the masked default sits at 1.6). The
re-measurement decides: above 10 fails, the inconclusive band is reported with its numbers.
On arm64 the run with PSTATE.DIT set is the gate, as the workflow always said; when the
runner's CPU has no FEAT_DIT (the GitHub arm64 runners of 2026-10), Arm gives no
data-independent-timing guarantee for the instructions, so that run is reported with its
numbers rather than gating: there the unmasked ML-KEM decapsulation shows a steady eight
ticks in eighty-four thousand between valid and random ciphertexts (|t| 13 at 900k
samples) that x86-64, Apple silicon (DIT) and the masked default do not show. The ACVP PBKDF sample exercises SHA-224 only; PBKDF2-SHA256 is checked
against OpenSSL and RFC 6070, and the README now says so.C_rmbl_hash_two_part (an internal self-test entry) refuses a non-integer or logical
split instead of coercing it.DESCRIPTION, SECURITY.md, the README and seven help pages without one having
taken place; it is back in all of them, and the masking sentence no longer appears on the
pages of HQC, XMSS and the DRBG, which are not masked.$attestation$note; the JSON codec
is R, not C, and is not a fuzz target; Windows randomness is RtlGenRandom;
capsule_bundle() pins whole files, the Merkle tree pins parts of one file; there is no
separate cloud-keys route; bricklayer_llm_ask() has no context argument; the SIU
schema column is name and an oversize tag is ignored, not an error; the synthetic marker
is the .synthetic sidecar; XMSS is not compared with OpenSSL; verification errors on an
object that is not a signature; ctgrind is a dynamic check, not a proof; the share
recombination sentence in the README names what signing publishes.yoy_pdf() drew rows off the page when height was not 8.5: the row height follows the
page height, as the pagination already did. rmbl_barrier.cpp is regenerated from its
generator. The www.rmorie.com move reaches data_hub.R and the services fallback, with
the trailing slashes the site expects. The offline vector subset is 3 MB, not 1.5.hill_tail_index() on a tail of identical values reported alpha = 25 (the optimiser's
bound) as reliable; such a tail is "not estimable", and an alpha at either bound is
flagged unreliable.mahalanobis_outliers(robust = FALSE) called columns of very different scale (1e160
against 1) collinear; the classical path standardises by the classical scale first (the
distance is affine invariant, so nothing else changes).Standards vectors, at scale
.github/workflows/vectors.yml, tools/vectors/): 6,848 tests checked, 0 failures. The
3,822 not applicable are named with their reason: bit-length SHA-3 messages (the API hashes
bytes), gigabyte messages, hedged ML-DSA signatures whose randomness the vectors do not
carry, and PBKDF2 over SHA-224 (the only hash the published ACVP PBKDF sample exercises;
PBKDF2-SHA256 is checked against OpenSSL and RFC 6070 instead). A subset of 3 MB ships in
tests/testthat/vectors and runs offline through the same runner.C_rmbl_mlkem_decaps accepted a decapsulation key whose stored
H(ek) did not match its own ek; FIPS 203 section 7.3 requires rejecting it, and it is now
refused.sha224, sha384, sha512_224, sha512_256, sha3_224, sha3_256,
sha3_384 and sha3_512 join sha256, sha512, shake128 and shake256. SHA-224,
SHA-512/224, SHA-512/256, SHA3-224 and SHA3-384 are new native digests, checked against
OpenSSL at every block boundary; each OID is bound into the signature, so two digests of
the same length (SHA-512/224, SHA3-224) never verify for each other.slh_sign_internal /
slh_verify_internal, not exported) so the ACVP internal vectors check the core the public
interface wraps.expected_counts() and sir() re-checked a sign the
input guard had already refused, drift_homogeneity() tested for a
condition its earlier check made impossible, mahalanobis_outliers()
re-checked a column count and inverted a matrix it had just made positive
definite, clean_column_names() handled an empty word list it had already
replaced, hill_tail_index() tested the finiteness of a sum of finite logs,
and the gzip writer re-validated memCompress() output.capsule_attest() with a raw
context signed the raw bytes but recorded them as hex, so
capsule_check_attestation() could never verify such an attestation; it
now signs what it records. gaussian_plume() and the other dispersion
models read a plain-vector receptors argument column-wise, so
c(x1, y1, z1, x2, y2, z2) was scrambled; it is now read as the x, y, z
triples the documentation describes.C_rmbl_hash_two_part, internal). The uncalled C
symbol rmbl_xmss_len() was removed. .bl_rd_db() and to_ascii() gained
seams so their fallbacks can be tested; compute_footprint()'s memory and
core detection likewise.getrandom() EINTR/ENOSYS and
/dev/urandom failure paths; the barrier's Rf_error() after an interrupt
(unwinds first) and its C++-exception branch (no kernel throws); the ECDSA
curve-width assertion and the point-at-infinity sum (prime-order curve); the
ML-DSA rejection-sampling refills, which a seed reaches with probability
below 1e-5; small_int()'s non-digit branch (every caller passes a \d
capture); and hexval()'s upper-case branch (the curve constants are
lower-case). Each is named here rather than hidden behind an exclusion.pg_sigmas(), briggs_plume_rise(), gaussian_plume(),
gaussian_puff(), advection_diffusion_2d(), lagrangian_particles()): the
Pasquill-Gifford coefficients with Briggs's fits, Briggs plume rise (buoyant and stable
branches, transitional rise), the Gaussian plume with ground reflection and mixing-lid
images, an instantaneous puff, an explicit upwind-advection/central-diffusion grid and a
Lagrangian random walk with a counting grid. Formula for formula the same as rmorie's
AirDispersion module; the particle walk takes any normal generator through normals=,
so rmorie's Philox paths can be reproduced exactly, and defaults to the package's shared
uniform stream. Every formula is recomputed in the tests.crf_pm25(), crf_no2(), attributable_fraction(),
mortality_displaced(), pollution_burden(), pollution_burden_by_area()): the WHO 2021
review's pooled estimates (Chen and Hoek 2020, RR 1.08 per 10 ug/m3 PM2.5; Huangfu and
Atkinson 2020, RR 1.02 per 10 ug/m3 NO2), Burnett et al.'s (2014) integrated
exposure-response curves for IHD and stroke, Levin's attributable fraction, the BenMAP
displaced-mortality function and the GBD burden chain, each citing its source and each
coefficient asserted in the tests. Mirrors rmorie's envhealth module.plr_crossfit(), exposure_response_plr()): the
partially linear model of Chernozhukov et al. (2018) by DML2 cross-fitting with
ordinary-least-squares nuisances and a sandwich standard error, deterministic from its
seed and recomputed step by step in the tests, plus a percentile bootstrap; any
estimator with the same interface (rmorie's morie_estimate_double_ml() among them) can
be passed.exposure_concentration_index()): the Wagstaff, Paci and van Doorslaer
(1991) concentration index of exposure by income.verify_pollution(), pollution_report_text()): demo data, a CSV, a
NAPS pull (NO2 ppb converted at 1.88) or scalars; an assumption log; one reference
concentration throughout; avoided deaths counted over the exposed share; exit statuses
0/1/2 for scripts.compute_footprint(), carbon_intensity(),
carbon_intensity_table(), detect_location(), rapl_available(),
footprint_equivalents()): the electricity an expression used and the CO2-equivalent it
implies, measured from the CPU packages' RAPL energy counters where the operating system
exposes them (codecarbon's Linux method; wrap-around handled, subdomains not
double-counted), otherwise modelled with the Green Algorithms formula (Lannelongue,
Grealey and Inouye 2021). The utilisation is measured, per process (this R process's CPU
time, codecarbon's process mode) or per machine (/proc/stat, codecarbon's machine mode),
or fixed by number; load_curve applies Green Algorithms' linear scaling or codecarbon's
curves. The grid's carbon intensity comes from a table bundled for offline use anywhere:
every country at the latest year Our World in Data publishes from Ember's yearly data
(2024 or 2025), sub-national zones from Electricity Maps' 2024 yearly data via Green
Algorithms data v3.1, and the world average, each row with its year and source; a
location is matched by code, alpha-3 code or name, an unknown zone falls back to its
country, and location = NULL detects it offline from the environment, time zone or
locale and records how. Every default that stood in for a measurement is listed in the
result. Cross-checked against codecarbon 3.3.1 and against rmorie's pollution module.kem_decapsulate() (ML-KEM-512/768/1024) and every
ML-DSA signature (capsule_sign() with an ML-DSA key, fips_sign_mu()) run
with each value that depends on the secret key held as two shares, refreshed with fresh
randomness from the operating system for every call. The output is byte-identical to the
unmasked computation: the full vector suite compares the two on every ML-KEM decapsulation
and ML-DSA signing vector. kem_decapsulate(masked = FALSE) keeps the plain path. Cost:
about 5 times the plain decapsulation and 16 times the plain signature (13 ms for
ML-DSA-65 on a laptop).src/rmbl_masked.h): Trichina's AND, a bitsliced Boolean adder,
arithmetic-to-Boolean and Boolean-to-arithmetic conversion modulo 3329 and 8380417, masked
Compress_1 and the decompressed ciphertext comparison (Bhasin et al. 2021), masked CBD
sampling, a masked Keccak, and for ML-DSA a masked SHAKE for rho'' and y, HighBits revealed
one bit at a time (w1 is public: it is hashed into the challenge), and the three rejection
tests revealing one bit each.inst/tvla, CI on every change): the gadgets and the ML-KEM
arithmetic, compiled for a Cortex-M4 and run in an emulator, are assessed with TVLA
(fixed-versus-random Welch t, two experiments) under two leakage models, the value
(Hamming-weight) model and the transition (Hamming-distance) model. Every masked kernel shows
no first-order leakage under either, and every unmasked control leaks (|t| up to 163). CI
fails on a masked leak under either model.inst/dudect, CI on x86-64 and arm64 Linux and on
macOS): the dudect fixed-versus-random test on ML-KEM decapsulation (plain, masked, and
each phase on its own), encapsulation, HQC decapsulation, the digest comparison and PBKDF2,
with an early-exit comparison as the control that must be flagged. Decapsulation is also
measured valid-against-invalid with both classes varying (fresh valid ciphertexts against
random ones), so the only systematic difference is the rejection bit: an arm64 runner had
flagged the fixed-ciphertext test at under one tick in 65,000 on one pass and not on the
next, while every phase measured on its own was clean.inst/fuzz/fuzz_ossl.cpp, libFuzzer): ML-KEM,
ML-DSA (valid and mutated signatures), SLH-DSA-SHA2-128f, the SHA-2, SHA-3 and SHAKE digests,
BLAKE2b-512, HMAC and PBKDF2 must agree with OpenSSL byte for byte.man/figures/logo.png, source data-raw/hex_logo.svg), on the README and the
package website.Two further reviews of 0.5.8 (the diff review and a dedicated SIU review) showed the
C-stack-overflow class still live from three sites the 0.5.8 rewrite did not reach, and
a correctness cost of the fix it did make. All closed here, each at every site of its
kind, pinned by test-review5.R and by fuzz seeds for every shape the reviewers used.
normalize_text() collapsed space and tab only; flatten_ws() had
the full six-character set 180 lines away. The first pass now collapses every
whitespace byte \s matches except the newline the second pass bounds.strip_boilerplate() ran two unbounded, newline-crossing regexes over the whole
document ([\s\S]*? to the privacy paragraph's end, [^.]* to the glossary
sentence's end); the line cap never bounded them and 44 KB of period-free text after
"this information may include", standard boilerplate in every real report, overflowed
the stack, from bricklayer_parse_siu(), bricklayer_siu_resolve_so() and the
network path bricklayer_fetch_parse_siu(). Both passes are find() scans now; no
regex in either SIU file crosses a line without a bound, and the hygiene test asserts
it over the sources.bricklayer_parse_siu(),
bricklayer_siu_text() and bricklayer_siu_resolve_so() warn how many lines were
split, because a field spanning a split can come back incomplete. 0.5.8 split at any
space, silently.strip_elements() rescanned to
the end from every unclosed <script, 40 minutes at the 2 MiB cap; strip_tags()
rescanned for > from every bare <). Both are linear, and the whole SIU path now
polls for a user interrupt through a hook the host installs (rmbl::check_interrupt()
here; rmorie's and morie's bindings install their own).bricklayer_siu_iso_date() errors past 4096 bytes instead of returning "";
small_int() reads up to nine digits (SO #1234567 is 1234567 again, not 1);
zero-padded numeric entities (é) decode; named entities are
case-sensitive, as in HTML and in 0.5.7.rmbl_pbkdf2_sha256() and rmbl_mann_kendall(), the two LinkingTo kernels that poll
for an interrupt, raise it from a frame that holds none of their buffers; 0.5.8 raised
from inside the poll, over live std::vectors. rmbl_interrupt_pending() never
raises now; rmbl_interrupt_raise_unbarriered() does, after the kernel returned. The
header documents rmbl_mann_kendall()'s NA-on-interrupt contract.capsule_drift()'s identifier heuristic gates on distinct values (20 or more), not
on a hundred rows a side: an 80-row id column is an identifier again, a 5-level
factor on ten rows still gets its verdict.mcar_test()'s log_p_value is documented; download_data(mode =) says what it
accepts; bricklayer_json_to_json() names keep_vec_names and quiet_vec_names;
MORIE_SERVICES_URL is documented and a mirror without a .json suffix finds its
.sig; bricklayer_download(timeout = 1e18) is an error, not an undefined cast;
a single-label host name is reported as a local address; bricklayer_llm_ask()'s
help says an own endpoint is taken when configured, not probed, and
bricklayer_llm_status()'s example switches the local route off.MORIE_LLM_BASE_URL) may be a literal private LAN address
over plain http (10/8, 172.16/12, 192.168/16, fc00::/7: an LM Studio or vLLM
box on your own network, the documented use), through the same per-call relaxation
the loopback host has; the diff review (D4) found it refused by two independent rules
with no way through. Link-local addresses and host names over plain http stay refused,
and a redirect off such an address meets the full policy. bricklayer_services()
documents MORIE_SERVICES_URL; bricklayer_parse_siu() and bricklayer_siu_text()
document the 2 MiB cap and the line split; capsule_drift() documents the identifier
gate; bricklayer_download() documents its headers and timeout ranges.fuzz_url target over url_check() and redirect_policy(), seeds for
every shape the reviewers used, and the SIU target runs under an 8 MB stack like R's.The fourth review (network and service discovery, 2026-10-06) confirmed the 0.5.8
transport closes every bypass it was built for, then found eight things at its edges.
Each is fixed at every site of its kind and pinned by test-review4.R.
options(rmoriebricklayer.allow_loopback = TRUE) relaxed every fetch in the process;
it is gone. The local-model routes pass allow_loopback down one call, only when
the address is the loopback host, and a redirect from a remote origin into the
loopback interface is refused. An endpoint of your own is either the loopback host
or a public https address.X-Api-Key, api-key, X-Auth-Token and the rest now stay
with the first origin. "Another origin" compares scheme, host and effective port,
so a redirect to another port on the same host drops them too..rmbl_json_text(), parses every network body, cached manifest and signature file;
the URL and file branches of bricklayer_json_from_json() are for a user's argument
only. A body or a .sig that is a bare URL is "not JSON", not a fetch.Bearer token and any sk- key removed before it reaches an R condition.MORIE_HOSTED_AUTH_URL is validated like every other endpoint, and the sign-in
service's verification_uri is opened in a browser only when it is a public https
address.bricklayer_download() has max_bytes (default
2 GiB) and the services document and JSON fetches cap at 1 MiB and 16 MiB; the
scratch file is private to the call and the body is moved into place on a 2xx only.The third review of the hardening work (2026-10-06) found seven things,
every one of them a fix that had landed at the site a reproducer named and
not at its siblings. This release fixes each at every site of its kind and,
where the siblings can be listed, puts the list in the tests
(test-review3.R, test-src-hygiene.R), so a new one cannot appear
unnoticed.
bricklayer_llm_ask() tries an endpoint of your own, then a local
Ollama server, then the hosted MORIE tier. MORIE_LLM_BASE_URL (with
MORIE_LLM_API_KEY, MORIE_LLM_MODEL) names any OpenAI-compatible
server; OLLAMA_HOST / OLLAMA_BASE_URL (default http://localhost:11434,
off to skip) a local Ollama, whose first listed model is the default
unless OLLAMA_MODEL says otherwise. route = "own" | "ollama" | "hosted"
insists on one. bricklayer_llm_status() and rmoriebricklayer doctor
report all three, in order; agent_bundle() follows the same order.
The hosted tier's addresses come from a signed document, not from the
package. bricklayer_services() reads
https://rmorie.com/.well-known/morie-services.json and its detached
ML-DSA-44 signature, verifies it against the public key pinned in the
package, caches it for a day under tools::R_user_dir(), refuses a
document older than the one it holds, and falls back to the cached copy,
then the copy bundled in inst/services/, then a document with every
service switched off. The endpoint, the model list, the sign-in address
and whether the tier is open can now change without a package release;
a document that does not verify is ignored. MORIE_HOSTED_BASE_URL,
MORIE_HOSTED_AUTH_URL, MORIE_HOSTED_MODEL and MORIE_DATA_URL still
override. The hosted tier is a last resort; keys are personal and issued on
request at www.rmorie.com/access/, and every hint in the package says
so. The GitHub and emailed-code sign-ins keep working.
Plain http on the loopback host is admitted for the local routes only.
The URL policy that guards every other address refuses http:// and
loopback twice over; the own-endpoint and Ollama calls set
options(rmoriebricklayer.allow_loopback = TRUE) for their own duration,
which admits exactly the loopback host (localhost, 127.0.0.0/8,
::1), nothing else private, and a redirect off it meets the ordinary
rules (test-llm-routes.R).
SIU core: every string-to-integer conversion on a regex capture goes through one bounded helper (small_int()); std::stoi on a tag such as SO #4444444444444444 threw std::out_of_range and aborted the process (found by the libFuzzer target; the input is in the fuzz corpus). Ported to the rmorie and morie copies.
bricklayer_siu_text() and bricklayer_parse_siu() killed R on 25 KB
of whitespace. libstdc++'s regex executor recurses once per character a
repeated atom consumes, so \s+ over a long run overflowed the C stack,
which no tryCatch() and no exception barrier can see. Every pass that
runs over a whole document (newline normalisation, script and style
removal, tags, entities, whitespace) is a plain loop now, byte-identical
to the regexes it replaced (the test keeps the old pipeline as an R
reference and compares); the text every field extractor sees has its
spaces collapsed and no line longer than 2,000 characters, so no remaining
regex can recurse further than that (and MSVC's regex, which has a step
budget instead of a stack, stays inside it -- the one time it did not, the
public entry points now answer "no match" instead of throwing); the input cap of every SIU entry
point is 2 MiB (a report page is a few hundred KB). The guard is a test
that enumerates every one-string SIU entry point from the namespace and
runs ten hostile inputs through each in a subprocess. XMSS key generation
polls for Ctrl-C with the throwing check like the other ten sites.
One attestation still verified two documents. 0.5.7 wrote a level
whose names were all empty as an array, so {"k":{"":"X"}} and
{"k":["X"]} shared a digest. The names attribute, present or absent, is
now the one thing that tells an object from an array at every level,
empty names included, and a named atomic vector is an object too
({"a":1} is not [1]). Attestations of documents whose fields are named
vectors change digest; capsules verified against a stored digest should be
re-attested.
Every network byte goes through the compiled transport.
bricklayer_download() gated with the validator and then transported with
base R's url(): no DNS check, no pin, no redirect re-validation, no size
cap on the package's own documented pipeline; bricklayer_json_from_json()
fetched a bare URL with no gate at all; download_data() wrapped
download.file(). All three use src/rmbl_fetch.cpp, with the live
progress bar driven by a callback from libcurl. The transport itself now
drops Authorization, Cookie and Proxy-Authorization on a cross-host
redirect, refuses a redirect from https to plain http even when the http
option is set, checks a URL before opening the destination and writes
beside it, moving the body into place only on a 2xx (a refused URL or a
404 no longer deletes a pre-existing file). The redirect rule is one pure
function with its own .Call entry and tests. A single-label hostname
(metadata, instance-data) is refused by shape, without DNS. A test
lists every network open in R/ and allows exactly the two base-R
fallbacks a standalone capsule bundle needs.
The interrupt flag. The barrier cleared rmbl_kernel_interrupted
only on the normal path; it is cleared on entry and on every exit now, and
a kernel called with no barrier active (a sibling package's own .Call)
re-raises the pending interrupt instead of eating it and returning NA.
rmbl_pbkdf2_sha256() in the C API returns an int (0, or -1 for an
argument it refuses); an interrupt inside it is raised, never a zero key.
capsule_drift()'s identifier heuristic applies its ratio test from a
hundred rows a side: a five-level factor on ten rows gets a verdict again.
mahalanobis_outliers() carries log_p_value, the fifth chi-square site;
log_p_value is documented at every site. drift_psi() accepts any
eps in (0, 1) again (0.5.7 refused values above 0.01, a new hard error
on a previously accepted argument) and the help explains what the floor
does to the index.
The Mann-Kendall exact-distribution memo is bounded; hostnames may carry
underscores; Makevars.win's libcurl fallback link line is wired in;
rmbl_digest_equal() gets the same value barrier as the ML-KEM select.
A release about measuring what the 0.5.6 README could only describe. Every claim in the new "Security posture" table is a check that runs in CI.
inst/ctcheck/ runs
ML-KEM, ML-DSA, SLH-DSA, HQC (v5 and round 4), XMSS, the CTR_DRBG, HMAC,
PBKDF2, keyed BLAKE2b, SHA-2, SHA-3 and the digest comparison under
valgrind memcheck with their secrets marked undefined (ctgrind), with GCC
and Clang; a branch or memory address that depends on a secret fails the
constant-time workflow. The run found and this release fixes: ML-KEM's
ByteEncode branched on every bit it packed (the secret key included);
the hex encoder used a table indexed by the secret nibble (every HMAC,
PBKDF2 and key output); ML-DSA's norm check returned at the first
offending coefficient; PBKDF2 round-tripped each MAC through hex and
branched on the characters; XMSS measured its hex-encoded seeds with
strlen(). The values the specifications publish (rejected samples, the
challenge, the hints, R) are declassified at one named line each.rmbl_ct::Guard), and the scan is part of the gate.ECDSA-Sig-Value was accepted with extra elements,
padded or negative integers, and that a PKCS#1 block was checked field
by field rather than against RFC 8017's exact DigestInfo bytes. All
three are fixed; every valid vector verifies, every invalid and every
legacy "acceptable" one is refused.inst/fuzz/ holds libFuzzer targets (ASan + UBSan) for the
DER parser and RSA arithmetic, the decimal-to-double conversion (compared
bit for bit with the C library), and the SIU report parsers, with seed
corpora; the fuzz workflow runs them on every change and for longer
weekly.SECURITY.md: threat model, what each check catches and does not,
reporting. The README's "Security posture" is now that table.rmbl_hmac_sha256_raw() joins the C API (the hex variant is a wrapper).The second review of 0.5.6 (2026-10-06) asked for each fix's neighbours.
Every item below has a test in tests/testthat/test-review2.R.
.Call entry point has the exception barrier, not two of 110:
src/rmbl_barrier.cpp, generated from init.c by
inst/scripts/gen_barrier.R, wraps each entry so a C++ exception is an
R error raised after every destructor has run. Seven NULLs to
hawkes_rescaled, a 2 GB shake, mismatched theil_sen lengths (a
40 MB out-of-bounds read), a 4 TB sen_slopes, a 2^31 reservoir and an
INT_MAX PBKDF2 are all plain errors now; the quadratic loops and the RSA
arithmetic check for Ctrl-C; the RSA public exponent is capped at 64
bytes (1 KB took 1.2 s, 1 MB would have taken 20 minutes, from a
certificate); shake output and PBKDF2 iterations are capped; every
string argument goes through one rmbl_str0(); the DER tree is converted
under R_UnwindProtect, so an allocation failure cannot skip its
destructor either.src/rmbl_fetch.cpp parses the
authority as a URL parser does (#@ and ?@ no longer hide a host, any
userinfo is refused), reads IPv4 literals with inet_aton's grammar
(127.1, 0177.0.0.1, 2130706433, 0x7f000001), IPv6 with
inet_pton (mapped, NAT64, link-local, unique-local), refuses local
names by suffix (localhost, .local, .internal, .localdomain,
.home.arpa), resolves every hostname and tests every address, pins the
connection to those addresses (CURLOPT_RESOLVE, so DNS rebinding
cannot answer differently), follows redirects itself one checked hop at
a time (an https fetch cannot be bounced to http or to a private
host), and caps a chunked download. bricklayer_fetch(),
.rmbl_read_json(), the CRL/OCSP fetch, the Wayback resolver and the
hosted-model client all go through it. .rmbl_check_public_url() calls
the same C check.verify_capsule(): provenance_file is contained like every other
path (a symlinked data_provenance.json verified tampered data as
intact); data_not_synthetic is a required row that always exists and
reads the sidecar, the manifest and the provenance; a manifest.json in
the capsule is checked by default and manifest_consistent /
script_sha256 are required whenever they apply; a sha256 or
filename that is not a single string, an unreadable file or a
directory are failed rows, not R errors.manifest_digest() is injective again: the canonical encoder writes an
empty key as "" (0.5.6 renamed it to its index, so {"":"X"} and
{"1":"X"} shared a digest and one attestation verified both). A list
with a names attribute is an object and is sorted, empty names included:
a manifest built with unnamed elements in a named list gets a different
digest from 0.5.5's, which never canonicalised such a level.
make_manifest() writes meta$synthetic only when it is TRUE, so a
real-data manifest keeps its pre-0.5.6 digest, and make_synthetic_csv()
sets the flag in-process.bricklayer_json_base64_dec() refuses characters outside the
alphabet instead of deleting them; whole numbers beyond 2^53 warn on the
decimal branch too; bigint_warn = FALSE and duplicate_keys = "keep"
are what the package's own API readers use; the gzip inflate cap is
64 MiB.capsule_drift(): a character column of dates is compared as dates
(two disjoint years drift); a column with more than identifier_levels
distinct values, or one per five rows, is an identifier -- its
unseen_share is reported and drifted is NA (the 0.5.6 PSI over the
union of categories fired on every id and date column); PSI for the rest
is over the reference's categories with unseen mass pooled. The result
gains type, unseen_share and note columns.drift_homogeneity() returns p_value = NA with
method = "inapplicable: ..." when the categories outnumber half the
observations (every table with those margins has the same statistic),
draws its Monte Carlo p-value under a local seed and restores the
caller's RNG, and like drift_chisq() refuses negative, NA and infinite
counts. All chi-square sites report log_p_value next to p_value: a
double has no number for exp(-2536), and the log says how far below
zero the tail is. cramers_v() drops an unused level (it returned NaN)
and takes a seed too.parse_bands(): trailing units with punctuation ("18 to 24 (years)",
"5 to 9 years, inclusive") parse again; cue words are matched as words
("overnight" is not "over") in the leading slot as well as the tail
("under 18 to 24" is NA, not 18-24); a label with two numbers that is
not a range is NA ("more than 10 - 20"); thousands separators are
removed only from correctly grouped numbers ("12,34,567+" is NA); a
label with a decimal point is on a decimal scale; the dash family is
normalised byte-wise, so an en dash parses in a C locale.
band_values() guards the open-lower floor as it guards the cap, and
its default floor is min(0, upper), so "under 0" has its midpoint
inside the band.trend_test() reports slope_ci_clamped = TRUE exactly when a rank
is at an end, warns when exact = TRUE cannot be honoured (n > 8), and
memoises the tied exact null; count_trend() refuses an all-zero
series; step_change() and count_trend() refuse infinite values as
trend_test() does; morans_i() refuses negative weights and a
constant variable; gini(), top_share(), lorenz() and
hill_tail_index() refuse empty input; hurwitz_zeta() returns the
Bernoulli values at non-positive integers (zeta(0, 1) = -1/2); the
Benford first digit of a subnormal is read from the shortest
round-tripping decimal (1e-310 is 1, not 9).morans_i() excludes
self-neighbours from a weight matrix; make_manifest() no longer
changes the digest of a real-data manifest (0.5.6 did); the OTIS
full-verification workflow runs on demand only.The coverage pass that followed (every branch below was untested at 96.2%) found five defects that no test had reached; the CRAN checks and the entry-point sweep found two more.
revocation_fetch() ran
every certificate through the single-file-path guard, so a parsed path
was refused before anything was fetched; the CRL/OCSP fetch refused
plain http, which is what RFC 5280 distribution points and RFC 6960
responders use (the answers are signed, the transport adds nothing);
and a delegated OCSP responder's certificate was never found, because
the search for the [0] certificates element landed on nextUpdate
inside the SingleResponse first and the signed answer was reported as
unverifiable. All three are fixed; the client is now tested end to end
against recorded OpenSSL responses (good, revoked, unknown, SHA-384
without an embedded certificate, a delegated responder, an ECDSA
responder, tryLater, and every malformed shape), with the two network
calls behind one-line seams. The fixtures are documented in
tests/testthat/x509-fixtures.txt.top_share() summed from the start for every fraction: a million
fractions over a million values was 10^12 additions with no interrupt
check (the entry-point sweep found it as the one hang). It is one prefix
sum now, interruptible.manifest_restore_seed() padded a record naming only the main generator
kind with NA, which RNGkind() rejects; the recorded names are passed
as they are.is.numeric(), which is
FALSE for a POSIXct, so a list of times was never folded back into
one vector.capsule_drift()'s date detection errored on a column of timestamps
with one unreadable value ("2020-01-01 25:99") instead of treating the
column as text.rmbl_barrier.cpp re-raised an interrupt through Rf_onintr(), which
is not part of R's API (CRAN refuses it). It now signals R's own
interrupt condition and invokes the abort restart, so
tryCatch(interrupt = ) sees it exactly as before.inst/scripts/gen_barrier.py, inst/fuzz/mkcorpus.py); they are R
now (gen_barrier.R, mkcorpus.R), with the same output byte for byte.
inst/ctcheck/ and inst/fuzz/ each carry a README saying what they
are and how to run them. The agent_bundle() example switches the hosted
tier off for its run rather than reaching the network when a key is
stored.wots_gen_pk() /
wots_sign() in the SLH-DSA body, rmbl_mad_constant(), three
unreachable guards in .rmbl_conc_input() / cramers_v() /
drift_chisq(), and a duplicated line in the JSON object writer. The
LinkingTo consumer test now calls the Weibull, Lomax and gamma Hawkes
kernels and the NaN/short-input paths of the statistics kernels.A security and correctness release from a full external review of 0.5.5 (every item below was reproduced against the installed package before it was fixed, and each has a test that fails on 0.5.5).
verify_capsule() fails when it cannot check. A provenance file that
lacked resource$sha256, resource$filename or the whole resource
block -- or was exactly {} -- verified a capsule with ALTERED data as
ok = TRUE, because an absent field appended no row and all() of the
remaining rows was still true. Every check that cannot be made is now a
FAILED row with the reason, ok requires the three required rows
(provenance_readable, data_present, data_sha256) to exist and pass,
a pinned script digest is checked whenever one exists, a non-CSV file
with a recorded row count or schema is a failed check, a
<data>.synthetic sidecar is a failed check, and the result has a class
and a print method that shows what was skipped.cert_parse()
(and every OCSP, timestamp and X.509 path that parses network bytes)
throw std::bad_alloc through std::terminate, killing the session
past any tryCatch(): a length field could be read as 2^64-1 and the
bound checks wrapped. Lengths are bounded without wrapping, the node
count is capped, a child must advance the cursor, and no C++ exception
can cross the R boundary (rmbl_asn1.cpp). The same discipline for
Rf_error() over live C++ objects in the RSA, BLAKE2b, SHAKE and digest
entry points..Call registrations are gone too. pack_sig() uses
memmove() for its aliased copy; the poly_chknorm() comment says what
the code does..Call entry that reads a string checks it is one (rmbl_str0());
random_bytes() prefers getrandom(2) where glibc has it; XMSS key
generation can be interrupted.resource$filename, bundle
paths and the pipeline's download target must be plain relative paths
inside the capsule (.., absolute paths, drive letters and symlinks out
are refused). Every URL the resolvers return and every URL
bricklayer_download(), friendly_download() and download_data()
fetch must be https (plain http only with
options(rmoriebricklayer.allowhttp = TRUE)), never file:// (an
offline test passes allow_file = TRUE), and never a loopback,
link-local, private or metadata address. The Wayback availability API is
queried over https; the snapshot URL it returns passes the same gate.
libcurl transfers are bounded: 5 redirects pinned to http(s), 2 GiB to a
file, 64 MiB into memory, and the "available": true check reads the
literal value.duplicate_keys = "keep" restores
jsonlite's behaviour); nesting is capped at 200 levels; a lone surrogate
escape is an error rather than the text "NA"; an integer above 2^53
warns once unless bigint_as_char = TRUE. bricklayer_json_unserialize()
rebuilds data types only by default and refuses functions, calls,
expressions, namespaces and S4 objects (trusted = TRUE for your own
session's JSON). manifest_canonical() sorts a level with an
empty-string key, so a re-serialised manifest keeps its digest.
json_gzip_decode() refuses a member whose ISIZE trailer declares more
than 512 MiB and checks the trailer after inflating.make_synthetic_csv() writes a
<path>.synthetic sidecar and returns a classed object; make_manifest()
and write_manifest_json() set meta$synthetic from
BRICKLAYER_SYNTHETIC themselves, and record() marks every comparison
INFO on a synthetic manifest, whatever the analysis script did.--allow-digest-drift is given, and then records the override in the
manifest. Its subprocess command line is fully quoted.capsule_bundle() hashes dotfiles and capsule_bundle_verify() lists
them; only the bundle's own file is exempt from "unlisted".MORIE_DATA_URL /
MORIE_HOSTED_BASE_URL must be https and public before a bearer key is
sent; the credentials file is created 0600 before the key is written;
a DRBG reseeds itself from the operating system in a forked or restored
process; the launcher install_cli() writes quotes its library path.drift_chisq(), drift_homogeneity(), benford_test(),
mcar_test() and the Mahalanobis screen); the KS p-value uses the
theta-function series for small t (two million-row columns differing in
one value were flagged as drifted); drift_psi() no longer collapses to
one bin on a constant or mostly-constant reference (a 1000x rescale
reported PSI = 0); drift_homogeneity() gives a Monte Carlo p-value
below five expected counts, and capsule_drift() reports a PSI and the
unseen-category share for categorical columns, so two disjoint date
columns are flagged; a one-category table reports df 0 and NA.
morans_i() honours the magnitudes of a weight matrix. The exact
Mann-Kendall p-value enumerates the observed values, ties kept.
eb_rates() refuses negative, missing and infinite counts.
parse_bands() strips thousands separators, reads "ages 18-24", and
leaves a bare "-5", inverted bounds, "100-200-300" and "15 to 19 and
over" as NA; expand_bands() errors (or warns, with
drop_unparsed = TRUE) instead of dropping units; band_values()
refuses a cap below a band's lower bound. count_trend() uses t
quantiles in its quasi-Poisson branch; trend_test() reports
slope_ci_clamped when the pairwise slopes do not reach the requested
confidence and refuses infinite values. Benford digits are read from
the decimal rendering (2e-300 was digit 1).graphics, grDevices and tools are declared; R CMD check fails CI on a WARNING; a configure script probes pkg-config /
curl-config for libcurl on Unix; an ASAN + UBSAN job runs the suite;
DESCRIPTION names every scheme the package implements and states that
none has been audited; the key and signature help pages carry a
Security section; a FIPS 202 known-answer test covers SHA-3 and SHAKE
directly; the LinkingTo consumer test calls all 45 published kernels;
the stale CHANGELOG.md is gone (NEWS.md is the record).XMSS: an index never signs twice. capsule_sign() signed two messages at index 0 when called
twice with the same key object (the key is an R value, so it cannot advance the caller's copy),
and the help said it refused. It now remembers, for the session, the highest index each key has
signed with and refuses that index or an earlier one (one key object signed twice, or a stale
copy), retiring the index before the signature exists (RFC 8391; NIST SP 800-208). A damaged
next_index (NA, negative, fractional) is refused in words. Across sessions, save the latest
key_state after every signature.
Hawkes processes: events at the same instant no longer excite each other. The intensity sums over
strictly earlier events, as the definition of a Hawkes process (a simple point process) has it;
the exponential recursion, the sum-of-exponentials recursion and the exponential and Lomax
densities counted a tie as an excitation at lag 0. core_hawkes_fit() warns when times are tied,
and core_hawkes_jitter() spreads times recorded to a resolution (daily dates) uniformly across
their interval, as Filimonov & Sornette (2015) recommend, with the splitmix64 uniforms rmorie and
morie use for the TPS fits (the same numbers on daily data). The goodness-of-fit KS test uses the
exact distribution only without tied residuals (the exact computation with ties ran for minutes
and exhausted memory).
core_hawkes_fit() is fast for the Lomax kernel: the projected BFGS now restarts its curvature
estimate when a parameter reaches or leaves its bound (as L-BFGS-B does). A Lomax fit to
exponential-like data drifts to the bound on alpha (its exponential limit), and the matrix learned
with alpha free mis-scaled the remaining steps: 1,164 iterations and 196 s on 9,839 events, now 64
iterations and 3 s to the same optimum. The same restart ends the gamma kernel's stall on its sum-of-
exponentials and truncated likelihoods (2,000 iterations to a worse optimum on 2,118 tied events;
now 2.8 s to the exact route's maximum), so "auto" keeps "soe" for gamma. core_hawkes_fit()
also returns at_bound, the parameters whose estimate lies on the box:
converged alone did not show that day-dated times had driven a shape to its wall. A fit whose event rate n / horizon lies outside what the
bounded baseline can reach (e^-15 to 100 e^15 per unit time; horizon = 1e308) is refused with
a message to express the times in another unit, instead of returning its start.
core_hawkes_nll() is the likelihood core_hawkes_fit() maximises, on the same parameters: its
Lomax was a power law with exponent alpha (a Lomax of shape alpha - 1), so its values and a fit
of it disagreed with the fit and with the documented density; it now uses the shared core for every
kernel, keeping its documented sentinel outside the feasible region. A non-finite horizon is
refused by every Hawkes function (Inf returned a list that looked like a fit).
SIU reports: counts in every layout. Investigators, forensic investigators, subject and witness officials and civilian witnesses are read from 2020-on English pages ("Witness Officials ( WO )", the singular "Civilian Witness ( CW )"), French pages ("Agents témoins ( AT )", numbered "AT no 1" or "AT n o 1", "Nombre d'enquêteurs de l'UES assignés : 3") and the legacy (2005-2011) layouts ("Witness Officer #2", "Three SIU investigators and two forensic investigators", "l'agent témoin n o 1", "Cinq enquêteurs"); "No civilian witnesses were identified" is 0, and with no subject-official section the resolver's rules decide (a bare "SO" no longer counts as one). Checked against the reviewed corpus on the 29 pages of the round-8 test: witness officials 27 of 29 (16 before), civilian witnesses 24 of 25 (14), investigators 29 of 29 (20), subject officials 20 of 20 (17).
SIU police_service: a service named as the one that notified the SIU ("the OPS contacted the SIU
to report that police officers with the VPD ...") is passed over for the subject officials' service
(drid 2010: Vancouver Police Department, not Ottawa); the OPP has one French name ("la Police
provinciale", a legacy header's detachment) and one English name ("OPP Sioux Lookout").
SIU dates: a notification that dates the incident relative to itself ("during his arrest two hours prior", "the day before", "deux heures plus tôt", "la veille") gives the incident date (drid 648: 2019-09-12, not an earlier break-in); legacy headers ("Incident date:", "Date de l'incident :") are read; the French narrative's "du 9 février 2022" is read and interview and team dates are passed over. Legacy pages are recognised by language. French abbreviated months ("5 janv. 2023"), upper-case accented months ("3 AOÛT 2017") and "March 3 , 2020" are read.
SIU dates: the incident date is the first dated sentence of the narrative that is not the
notification or the SIU's own work (drid 670: "... contacted the SIU to report a serious injury. On
July 8, 2019 at about 3:50 p.m., CKLPS were called" is 2019-07-08), "of
SIU fields: specific_injuries comes from the investigation, never from the mandate's definition of
a serious injury, with whole words ("stab" is not "constables", "arm" not "firearm"), more body
parts, and French injuries ("fracture de l'épaule droite"); French charges_recommended and
relevant_legislation ("Code criminel") are read. À, «, »,   and
the other capital and French entities decode.
bricklayer_siu_resolve_so(): the spelled-out plural ("Two subject officials were designated"), "no
official was a subject official" and witness officials with no subject official named resolve, as
the help describes.
bricklayer_siu_resolve_so() reads French reports: the "Agent(s) impliqué(s)" roster ("AI no 1", a
lone "AI"), legacy ordinals ("agent impliqué n o 3", "l'agent(e) impliqué(e) n o 1"), "aucun agent
impliqué" and witness officials (AT) alone; the privacy, definition and legal paragraphs ("des agents
impliqués et témoins", "les agents impliqués sont invités ...") are not read as cues. It returned NA
on every French page. bricklayer_parse_siu() falls back to these rules for a French report with no roster,
as it does for an English one (drid 3658: 9, as its English twin).
Inputs: kem_keygen(512.5), kem_keygen(c(512, 768)), pqc_keygen(2.5) and every other whole-number
argument refuse a fractional value instead of truncating it to another setting;
bricklayer_fetch_siu(1.5) no longer fetches drid 1; bricklayer_parse_siu() refuses a path that
does not exist (it parsed the string as an empty report); bricklayer_fetch_parse_siu(lang = "xx")
is an argument error.
Argument errors in words where R's internals spoke: bricklayer_json_base64url_dec(character(0))
(now empty, as bricklayer_json_base64_dec()), bricklayer_json_unserialize() of a number,
capsule_bundle(character(0)), yoy() of an infinite value, HyperLogLog registers beyond 64
(distinct_count(1e300)), and bricklayer_fetch_parse_siu() with an invalid drid, which is now an
argument error before any network instead of a message and NULL.
core_hawkes_jitter(horizon = ) spreads each event over [t, min(t + resolution, horizon)], so a
time dated on the horizon stays inside the window (it was pushed past it, and the fit then refused
the times); the fit's refusal names the span and the remedy.
The ?mahalanobis_outliers example contradicted itself: its "ordinary on each variable" row had
the largest weight, and its classical-versus-robust comparison printed FALSE. The row is now
short and heavy within both ranges, and the comparison uses a cluster of bad rows.
A count argument refuses TRUE (it was read as 1: drbg_generate(d, TRUE) gave one byte), Inf
and values beyond the integer range in words, with no coercion warning; hqc_sizes(Inf) no longer
warns before refusing.
bricklayer_fetch() gives the HTTP status of a server that answered ("answered HTTP 404"; it said
"could not reach"); friendly_download(attempt_wayback = FALSE) (or "") says the fallback is
switched off instead of claiming the Wayback Machine has no snapshot.
data pull refuses an --out path it cannot write before downloading the table.
CLI exit codes: a bundle with no language-model route exits 1; an empty prompt or request, a
malformed --email and an unknown option to help exit 2.
bricklayer_json_to_json() is byte-identical to jsonlite's toJSON() where jsonlite does not
pass an option on: raw = "js" is written with jsonlite's defaults (new Uint8Array([35]), an
array, even under auto_unbox, where it wrote the number 35), raw = "hex" is unpadded ("f"), a
Date epoch never gets a decimal point, the mongo $date and $binary objects and
keep_vec_names objects stay on one line under pretty, and keep_vec_names does not reach the
columns of a data frame. 18,000 random objects now encode identically (30 in 3,000 differed).
bricklayer_json_serialize() is no longer called lossless: like jsonlite's serializeJSON() it
rounds doubles to digits (8); digits = I(17) round-trips them exactly. ?bricklayer_llm_login
says a token is checked with the gateway before it is stored.
CI checks on Intel macOS (macos-15-intel), where r-universe's check had failed.
hqc_compress_key() stores an HQC secret key as its seed alone: for v5 the specification's
compressed format dk = seed_KEM (32 bytes), for round 4 the 96 to 112 bytes its key generation
draws (round 4 defines no compressed format). hqc_decapsulate() takes such a key, re-derives the
pair and refuses a seed that does not give the key's own public half.
The HQC documentation no longer calls FIPS 207 a draft: NIST has named the standard but had published neither it nor a draft by 2026-10-05.
core_hawkes_fit(method = "em") finishes with the projected BFGS on the exact likelihood from
EM's point. EM converges linearly, so its steps shrink before it reaches the maximum: it stopped
up to 1.5e-5 short of the direct fit in the negative log-likelihood (1.4e-5 on macOS x86_64,
where r-universe's check failed); it now ends within 2e-8. converged reports whether that
finish converged instead of always TRUE.
SIU police_service is now the service of the subject officials, read from the director's analysis:
the first service named in a sentence that names a subject official (or the sentence before it),
else the service the analysis names most. It was the force that notified the SIU, which is often a
custody, requesting or neighbouring service -- on 93 French reports it disagreed with the reviewed
English report. The case number's letter (T Toronto, P OPP, I First Nations, O any other service)
rules out services of the wrong kind, and legacy reports' "Police service:" header decides for them.
Checked against every reviewed report in the corpus: 2,147 of 2,163 French and 111 of 118 English
agree; the French values the old rule wrote agreed on 2,097 of 2,191.
SIU reports name the same people "subject officers" (before the SIU Act, 2019), "subject
officials" (after it) or "SO": the parser now writes the count as number_of_subject_officials,
the corpus's and the SIU Act's term and the same form as number_of_witness_officials (it was
number_of_subject_officers, so the parsed row and the shipped corpus disagreed on the column
name). The section and ordinal matching already read all three spellings.
A deterministic random bit generator: drbg_new(), drbg_generate() and drbg_reseed() are the
CTR_DRBG of NIST SP 800-90A with AES-256 and no derivation function, written here (AES computed in
constant time, with AES-NI on x86-64). It reproduces all 720 AES-256 no-df vectors of NIST's DRBG
validation suite (no reseed, reseed, prediction resistance) on both ciphers, and it is the
randombytes() of NIST's rng.c: seeded with the bytes 0..47 it gives the seed of vector 0 of
the post-quantum known-answer files written with it.
HQC in both revisions: hqc_keygen(version = "round4") adds the fourth-round submission of
2023-04-30 (HQC-128/192/256, the HQC of liboqs up to 0.12 and of PQClean) with its 64-byte shared
secret, next to the default v5 (2025-08-22, 32 bytes). It shares v5's codes and parameters and
reproduces all 300 of that revision's official known-answer vectors on both multipliers. Keys
carry their version, hqc_encapsulate() / hqc_decapsulate() follow it, hqc_sizes() takes
it, and a round-4 secret key whose public half is not the one its seed makes is refused. A level
must be one whole number (c(1, 3) and 3.5 were read as 1 and 3).
Hawkes processes fitted fast and exactly: core_hawkes_fit() (constant or sinusoidal baseline;
exponential, Weibull, gamma or Lomax kernel) maximises the likelihood with its analytic gradient
by projected BFGS in C++, and core_hawkes_residuals() gives the time-rescaling residuals in O(n)
or O(n w). Routes: "exact" (Ozaki's O(n) recursion for the exponential kernel; the Weibull and
gamma double sums stop where the kernel underflows to 0, so they equal the full sums), "soe"
(Lomax and gamma with shape < 1 as sums of exponentials with relative error eps, Beylkin &
Monzon 2010), "truncate", "em" (Veen & Schoenberg 2008) and "inar" (Kirchner 2017). The code
is shared with morie's Python arm, which calls the same routine, so the two return the same
estimate (asserted on common data in both test suites). core_uniforms() exposes the splitmix64
stream both arms use where they must draw the same numbers. The default method = "auto" is exact
for the exponential kernel, "truncate" for Weibull (whose exact window, where the kernel
underflows, spans the whole record when the shape is below 1) and "soe" for Lomax and gamma;
"soe" truncates a gamma kernel with shape >= 1 at eps.
HQC-KEM, the code-based key encapsulation NIST selected in March 2025 beside ML-KEM:
hqc_keygen(), hqc_public_key(), hqc_encapsulate(), hqc_decapsulate() and hqc_sizes() at
HQC-1, HQC-3 and HQC-5, implemented in the package. All 300 official known-answer vectors of the
specification of 2025-08-22 (reference implementation v5.0.0) are reproduced byte for byte, on
both the hardware (PCLMULQDQ / PMULL) and the portable carry-less product, and on big-endian
machines. Secret-dependent work is branch-free with no secret-indexed memory or shift (checked
with valgrind), secret intermediates are wiped, a ciphertext that fails the FO check yields the
implicit-rejection key, and a decapsulation key whose seeds do not derive from each other is
refused.
Fresh-user round 5: VERB --help prints the verb's usage under both names and never runs it
(logout --help used to forget the key); messages name the command typed (rmbl or
rmoriebricklayer); usage errors exit 2 and an unknown function in describe / examples exits 1;
--version works. login --token asks the gateway before storing a key and refuses a rejected one;
login --email checks the address and --code needs --email; the GitHub sign-in says it is still
waiting every 30 s. A key the gateway rejects is reported as rejected (not "gateway not reachable"),
the gateway's quote of the key and its hash is never printed, and models lists the models without
the suggested jsonlite. agent_bundle() and doctor use the package's own hosted route only
(backend is "auto" or "hosted").
bricklayer_fetch() says why a download failed (the HTTP status, a server never reached, no
Wayback snapshot) and refuses a non-http(s) URL, an unwritable destination or a non-positive timeout
before downloading; friendly_download() keeps R's url() warning out of the console, reports its
status as the cause and says when the Wayback Machine has no copy; the Wayback lookup retries once.
SIU: an impossible date ("February 30") is no date, day-first ordinals ("2nd February 2018",
"22nd of March 2019") and abbreviated months read, the hex quotes and dashes decode as the decimal
ones, report pages are read from their body rather than the table of contents (incident dates were
empty), and the helpers take NA and several reports. yoy() compares a biennial series step to
step (it found no comparisons) and labels an inserted year "no data"; report_analysis() prints
years without a thousands separator and describes a percent analysis in percentage points;
sha256_file() names a missing file.
The SIU parser reads ordinal dates ("August 3rd, 2017") as well as French months, and legislation
such as the Police Services Act no longer outvotes the police service (17-OVI-201 reads Guelph Police
Service, 2017-08-03). data --help exits 0 like every other help, and bricklayer_fetch() refuses
an empty URL or path in words.
The command line answers to rmbl as well as rmoriebricklayer: install_cli() writes both
launchers, pinned to the library they were installed from with .libPaths() inside R (an R_LIBS
in the shell or ~/.Renviron cannot swap in another copy) and without the explicit --args that
made R 4.6 report "unknown verb '--args'"; login --help prints the usage instead of starting the
sign-in.
Fixes from the 1.4.0 stress test of the family: friendly_download() no longer fails inside its own
Wayback fallback when the snapshot lookup returns nothing usable, and names a cause for an error it
does not recognise; wayback_snapshot_url() returns NULL for a malformed answer; yoy() averages
several rows of one period for continuous values and percentages (counts still add up; pass fun
to choose); report_analysis() renders a percent analysis (its changes are percentage points);
the SIU parser reads French dates ("3 ao\u00fbt 2017") and decodes accented named entities
(ç, é, ...) and every numeric entity.
The hosted tier lists additional AI models (kimi-k2.6:cf, kimi-k2.7-code:cf, deepseek-v4-pro:cf, deepseek-v4-flash:cf, glm-5.2:cf, glm-5.3:cf, glm-5.3-flash:cf, gpt-oss-120b:cf, gpt-oss-20b:cf, llama-4-scout:cf, qwen3.8-27b:cf, nemotron-3-120b:cf and gemma-4-26b:cf) beside the
ollama.com ones, and falls back to them when a cloud model is rate
limited; rmoriebricklayer ask --model gpt-oss-120b:cf picks one.
The rmoriebricklayer launcher works under R 4.6, which keeps --args in
commandArgs(); friendly_download() goes through bricklayer_download().
bricklayer_download(): one download routine with a live progress bar
(percent, size, rate) on a terminal and milestone lines in a log, used by
friendly_download() and the data hub. options(morie.quiet = TRUE)
silences it.
cert_chain_verify() and timestamp_verify() verify SHA-384 signatures
(ECDSA-SHA384, RSA-SHA384): the digest was computed natively but never
routed to the verifier, so such chains stopped with an error.
bricklayer_data_manifest(), bricklayer_data_tables() and
bricklayer_data_load("db/table"): the 160 databases the MORIE project
materialises from BigQuery public datasets, served from the edge and opened
by the key bricklayer_llm_login() stores. From the shell:
rmoriebricklayer data list, rmoriebricklayer data pull db/table.bricklayer_llm_models() returns the models the hosted MORIE tier offers
your key (through the package's own libcurl GET), with the default
bricklayer_llm_ask() would use as an attribute.rmoriebricklayer models prints that list (default marked);
rmoriebricklayer doctor names the models on its hosted line;
rmoriebricklayer ask --model NAME PROMPT picks one for a single call.bricklayer_llm_ask(), bricklayer_llm_login(), bricklayer_llm_logout()
and bricklayer_llm_status() talk to the hosted MORIE LLM tier at
llm.rmorie.com (retired in 0.5.12) through the package's own libcurl POST. Sign in
with the GitHub device flow, a code sent to your email, or a key you
already have; the key is kept in $XDG_CONFIG_HOME/morie/credentials.json
(owner-only, written only by an explicit login), the file morie and
rmorie read as well, so one sign-in serves all three. Nothing is
imported from either.agent_bundle() answers through that route when a key is stored and only
then falls back to the optional rmorie-cli agent; it recognises the
launcher that the rmorie R package installs under the same command name
(which has no agent verb) and skips it.bricklayer_cli() and the inst/bin/rmoriebricklayer launcher give the
package a shell command; install_cli() links it into ~/.local/bin
(a .cmd wrapper on Windows), only when called. Verbs: login,
logout, doctor, ask, bundle, functions, describe, examples,
version, help.latest follows release tags.The Ontario Human Rights Commission's 2020 report A Disparate Impact
stated that Black civilians in Toronto were 30 to 58 times as likely as
White civilians to experience police use of force; the corrected figure
(OHRC correction of 26 January 2023, after Maria Jung's independent review)
is 4 to 5 times, because the four race codes had been rotated during a
transfer from SPSS to R (White read as Black, Black as other racialized,
other racialized as unknown, unknown as White), and the transfer took the
blame for two and a half years. Thirteen functions make that
class of error impossible to commit silently and easy to name after the
fact: guard_recode() maps by name and refuses anything unmapped,
decode_codes() decodes imported integer codes against an explicit
dictionary, guard_levels() fixes levels and the reference group,
audit_categories() flags the import hazards, verify_recode() proves
a recode by cross-tabulation, verify_marginals() checks recoded counts
against published counts and names the label permutation that would
explain a mismatch, odds_ratio_check() recomputes reported odds ratios
under every relabelling of the table and names the one that reproduces
them, guard_binary() stops a factor reaching a numeric treatment slot,
and recode_manifest() with write_recode_manifest() and
verify_recode_manifest() records the chain, signs it with a
[capsule_sign()] key, and verifies it later. The same guards ship in
rmorie and morie (both arms), verified three-way. New vignette:
Categorical integrity.
The transfer itself is guarded and, when blamed, examined. relabel()
maps old labels to new ones BY NAME and refuses a positional vector of
labels, which is the one-line idiom (levels(f) <- sort(labels)) that
produces exactly the documented rotation from codes 1 to 4.
decode_labelled() decodes a haven-style labelled import by code, with
the levels in code order. transfer_verify() takes what the source
program printed (its code book and its frequency table) and refuses an
import that does not reproduce both. relabel_forensics() names which
mechanical step (alphabetical, reversed, rotated, frequency-ordered,
string-sorted codes) reproduces an observed permutation, so a transfer can
be exonerated, or not, from the code book alone: no import routine sorts
value labels onto codes. audit_categories() also flags code-prefixed
labels such as "1. White".
CRAN's gcc-UBSAN run on 0.5.0 reported two undefined operations, both
fixed: ML-KEM's Montgomery reduction multiplied two 16-bit unsigned
values in int (overflow above 2^15); it now multiplies in uint32_t.
HMAC with an empty key passed a null pointer to memcpy for zero bytes;
the copy is skipped.
The bundle built by make_bundle.sh runs on a machine with none of the
family packages installed, so every internal name a staged file calls has
to be staged too. Three were not: the input guards the libraries now
call, the seed helper behind the synthetic-data route, and the compiled
core_sha256() behind manifest_digest(). The first two files are now
staged; manifest_digest(), .rmbl_read_json() and the describe table
fall back to the pure-R implementations when the compiled ones are
absent, with the same numbers. dev/bundle_symbol_scan.R now runs inside
the build and fails it on any called-but-unstaged name, so the class
cannot recur. setup_and_run.R --synthetic (or OTIS_MRP_SYNTHETIC=1)
takes the synthetic route without a prompt, and the exhausted-downloads
message names that route by its real menu number.
verify_capsule() failed on every intact capsule: the pinned digest
arrives from provenance JSON as a length-1 list and was compared to the
computed string with identical(). verify_sha256() now coerces the
digest and compares case-insensitively.core_ipw_weights() refuses a treatment that is not coded 0/1 (a 1/2
coding used to send every unit down the control branch) and a
propensity outside [0, 1] (previously clipped to the trim bound); a
missing treatment or propensity gives a missing weight.NA to NA; signatures, HMAC and key derivation
need character or raw messages; stock-and-flow measures refuse Inf and
numeric dates; band parsers need character labels; rule constructors
need a column name; manifests need lists; the download, wayback,
revocation and capsule verifiers need non-empty paths, and
verify_capsule() an existing directory.sir() reports excess only when the interval lies above one, plus
deficit and significant; published_bounds() labels a bare NA
"missing" instead of "exact".examples/otis-mrp/analysis.R prefers rmorie for the DML recompute, as
its header says, and falls back to DoubleML.core_cor(): the shared kernel now uses the centred two-pass formula
and clamps to [-1, 1]. The one-pass expansion was wrong at the second
decimal for a spread of 1e-7 of the mean, NaN by 1e-8, and returned
|r| > 1 at 1e-15; cor(x, x) could come back -1.verify_marginals() and transfer_verify() gain strict. The default
still errors on a mismatch; with strict = FALSE the result comes back
with ok = FALSE, the permutation that explains the counts, a
message, and (for transfer_verify()) reasons, so
relabel_forensics() is reachable from the public path. The documented
ok = FALSE was previously unreachable.audit_categories() flags leading/trailing whitespace including
non-breaking spaces, whitespace-variant duplicates, empty-string
labels, missing-value sentinels stored as labels ("NA", "N/A", "NULL",
...), and a reference level that is any of those. Five such columns
used to pass as "no hazards detected", two of them with the invisible
variant as the reference level.relabel_forensics() reports an identity permutation as "no
permutation to explain" instead of naming a mechanism.scan_adjust() refuses p-values outside [0, 1] instead of adjusting
them and calling a negative value significant.The Sen confidence interval enumerated every pairwise slope in an R
double loop that grew a vector one element at a time, so a series of
2,000 periods took minutes and a long one never returned. The slopes are
now enumerated and sorted in C (n of 3,000 runs in seconds, identical
interval), and trend_test() refuses more than 20,000 periods with a
message giving the memory the pairwise slopes would need.
drift_calibrate(), capsule_power(), capsule_falsify(),
falsify_family(), the synthetic-data generator and the trend bootstrap
seeded the session and left it seeded, so a user who had set a seed for
reproducibility got identical downstream draws whatever seed they chose.
Each now seeds for its own call and restores the caller's stream on exit;
the seeded results are unchanged.
core_moments() squared its first value on the first step of the
single-pass update, so any input above the square root of the largest
double gave NaN for every statistic, and raised raw deviations to the
fourth power, which overflowed beyond about 1e77. It now takes two passes
on deviations scaled by their largest magnitude; the definitions and the
results on ordinary data are unchanged, and the shape statistics are NaN
only when the variance is zero. The streaming accumulator behind
rmbl_moments_acc_add() no longer squares its first value either.
json_gzip_decode() refuses input that is not a gzip member (fewer than
18 bytes or not starting 1f 8b) with a clear error. It used to pass the
bytes straight to memDecompress(), which in R 4.6 dumps core on an
empty vector, so json_gzip_decode("") crashed the session.
The shared numeric core summed naively, so core_mean(rep(1e308, 3)) was
Inf and core_mean(rep(1e120, 3)) was off by 1.4e104; core_var(),
core_sd() and core_moments() inherited it, returning 3e208 for the
variance of three identical values. The core now uses base R's algorithm
(extended precision sum plus one corrective pass) with a running mean as
the fallback when the sum overflows although every input is finite, and
the same fix ships in rmorie, which carries a copy of the header. Results
on ordinary data are bit-identical to mean().
analyse_table() runs the questions asked of every published table of
counts (what is in it, what changed and how sure, rates if there is an
exposure, trend, drift against the prior capsule) and returns one object;
report_analysis() writes it as Markdown or a single HTML file;
use_capsule_template() writes a capsule folder whose analysis.R runs
as written. A getting-started vignette walks the shipped OTIS table
through it in twenty lines.
published_bounds() turns rounded (rounding = 5, nearest or Statistics
Canada random rounding) and suppressed ("x", "<5") cells into the
interval of observed counts that could have produced them;
change_envelope() carries that interval through a difference, a percent
change or a rate exactly; yoy_bounds() adds it to a yoy() table with
a combined interval that is the union of the sampling interval and the
envelope. No confidence interval covers this uncertainty, so it was
previously invisible.
yoy_pvalues() gives the exact conditional-binomial test that matches
the yoy() interval; scan_adjust() adjusts a yoy() or rate_change()
scan for multiple comparisons (Benjamini-Hochberg by default) and marks
what survives; rate_change() now returns previous_count and
previous_population so the test is exact given the exposures.
drift_calibrate() estimates how often capsule_drift()'s screens fire
on identical data by splitting one release into random halves, per
column and family-wise, with the per-screen alpha that would hold the
family-wise rate.
ascii_fallback() and to_ascii() test the bytes with validUTF8() and
drop invalid ones with an explicit UTF-8-to-UTF-8 iconv(). The previous
guard went through enc2utf8(), which under a C locale re-encodes invalid
bytes as Latin-1 instead of flagging them, so the fallback returned them
untouched. bricklayer_json_minify() and bricklayer_json_prettify()
declare their result as UTF-8 (paste() had dropped the mark, so
nchar(x, "chars") over-counted in a C locale). Found by an independent
C-locale check of 0.5.0; the check matrix now includes a C-locale cell.
The JSON byte-order-mark test and strip work on the bytes, so a C locale
no longer warns "unable to translate '<U+FEFF>...'" or "invalid char
string in output conversion" while validating or minifying JSON.
A region map from facilities to statistical regions is built once and read
many times, so an error in it is the one error recomputing the downstream
tables cannot find: everything downstream reads the region map, and both
sides of any comparison move together. R/region_map.R adds the checks
that can find it.
region_coverage() reports how many regions hold at least one unit and
what share of the population lives in them, and its print method says
each time that the share is not a rate denominator. A region holding no
unit is not an unserved population: units serve catchments, which a
point location does not state. Summing the populations of unit-holding
regions pairs a partial denominator with a numerator drawn from the
whole territory, and every rate built that way is inflated unevenly.
region_map_integrity() finds what a comparison against published
output cannot, because it would be present on both sides: a unit
assigned two regions, a unit assigned none, a region code belonging to
another province. No geometry, so it runs with nothing installed.
region_map_compare() matches two region maps on the unit identifier and
compares them cell by cell, numerics through all.equal() so a
coordinate that survived a round trip through text is not reported as a
change.
region_map_second_route() compares the assignment against one derived a
DIFFERENT way, and takes the known-bad cases by name rather than by a
loosened tolerance. This is the only one of the four that can catch an
error in the original method, because it does not use that method.
region_map_from_points() recomputes the assignment by point in
polygon, projecting the points onto the boundary file's own coordinate
system rather than the reverse. It returns NULL without sf or
without the boundary file, so a verification script records the check as
unavailable instead of failing over an optional dependency. sf joins
Suggests.
New section 3g re-derives the institution to census division region map and the division populations under it, rather than reading them. The 49 Ontario census-division populations re-derive exactly from Statistics Canada 17-10-0139-01, summing to 15,109,416; the 21 divisions holding an open institution hold 10,110,752 of those. Twelve checks run with nothing granted, and injected errors were confirmed to fail them: a moved institution, a foreign region code, an altered population, a dropped division and a duplicated row each break at least one.
The city-name second route covers eleven of the twenty-five open institutions and disagrees on one, which is recorded as documented rather than tolerated. Sudbury Jail stands inside the City of Greater Sudbury, census division 3553 and 171,568 people; the bare name "Sudbury" belongs to census division 3552, Sudbury District, 22,746 people. Two places, one name. The geometry is right and the name route is wrong, and a SECOND disagreement is still a failure.
institution_cd_region_map.csv and cd_population_2022.csv now travel
with the example, so the checks run from the bundle.
The push CI now fetches the public OTIS A01 input (4.7 MB, cached on
its pinned provenance) and passes it to the smoke test, so every section
of analysis.R is exercised on each push instead of recording as INFO.
The bundle's R packages are read from config.json rather than listed a
second time in the workflow.
verify_bundle.sh compares the manifest against config.json's
total_checks and pass_csv whenever real data was supplied, and fails
on a mismatch. The expected counts are now an assertion, not a note.
A weekly workflow, otis-full-verify.yml, runs analysis.R with every
opt-in verification on: the 147 published year-over-year tables, the
108 rate tables, the census-division populations re-derived from
Statistics Canada, and the point-in-polygon recompute of the region map
against the 2021 boundary file. It fails on any DIFFER and keeps the
manifest for ninety days, so "verified" carries a date.
CI refuses a copy of any vendored R file tracked under examples/, the
shape of the custody.R shadowing below.
check_pkgdown() runs in the test suite when pkgdown is installed, so a
topic missing from the reference index fails locally rather than on the
site build.
rate(), share(),
rate_change(), stock_flow(), region_coverage() and
band_sensitivity() -- print through one frame: a titled rule, the
table, the lines that qualify it. The caveats each of them carries are
laid out the same way instead of rewritten per method.examples/otis-mrp/custody.R is removed. It duplicated R/custody.R
and took precedence over it: analysis.R searches beside the script
before ../../R, so a run from a checkout without the package
installed resolved to a stock_flow() without the baseline argument
and without period_days() or stay_summary(). It was the only one of
the nine vendored R files tracked under examples/, and
make_bundle.sh copies the package file over it at build time, so
built bundles were unaffected.sprintf("%.17g") delegates to the C library, and Windows aarch64's
gets it wrong: it renders the largest double as
1.7976931348623156e+308, one ulp low, so the text reads back as a
different number. Eight assertions in test-attest.R and
test-json-edges.R failed there while passing on every other
platform, and jsonlite, which brings its own converter, disagreed with
both. The digits are now generated by exact integer arithmetic in
src/rmbl_strtod.cpp, the same way the reader already worked:
mantissa times a power of two, scaled by an exact multiply and an
exact division, with the remainder deciding the rounding, ties to
even. Verified byte-for-byte identical to the platform across 199,934
doubles wherever the platform is correct, so nothing changes on a
sound library.Five exported functions -- adp(), alos(), admissions(),
adp_from_counts(), stock_flow(), and then period_days() and
stay_summary() -- were added across three commits that all carried
Version: 0.4.7. r-universe had already built and served 0.4.7 from an
earlier snapshot, so a package depending on rmoriebricklayer (>= 0.4.7)
could be handed a build without any of them, and rmorie's minimal-dependency
job duly failed on exactly that. A version number that does not move when
the interface grows cannot be depended on, which is the whole reason it
exists. Everything below was previously listed under 0.4.7.
Both were computed and published for reading before they were shipped
anywhere a check could see them. They are in analysis.R now.
Section 3e computes Lakner's measures for segregation through the
package's own adp(), alos() and stock_flow(), so the example
exercises the functions rather than reimplementing them. It is NOT
gated, unlike the rate tables: there is no denominator to argue about,
since days divided by 365 and days divided by the people who served them
are both unambiguous.
It records checks that can fail, not just values:
lakner_b02_one_row_per_person asserts the structure that makes
TotalAggregatedDays_Segregation Lakner's X-sub-i.lakner_people_b01_vs_b02_* requires the two tables to agree on how
many people there were.lakner_decomposition_exact requires (1 + p) * (1 + l) - 1 to
recover the change in days, to 1e-9.lakner_b01_consecutive_is_not_total_* records the ratio, about 0.67,
so the trap cannot be rediscovered the hard way: b01 holds roughly
2.7 rows per person and its NumberConsecutiveDays_Segregation is a
spell length, not an additive share of the year.lakner_day_share_* puts the day share beside the headcount share,
which is how Lakner p.15 says a subgroup share of the average daily
population must be taken.Section 3f is the capacity analysis, as a pair rather than an
imputation. Operational_Capacity is missing for three of the 25 open
institutions and they are the large ones -- Central East CC, Central
North CC and Toronto South DC -- while the largest observed capacity is
944. Missingness that depends on the value is MNAR, not MAR, so imputing
would pull all three toward the observed mean and understate three
denominators in a knowable direction.
So Central and Northern, whose capacity is fully observed, get a
complete-case estimate, and the other three get a range across the
plausible span of their one missing capacity. Three checks carry the
argument: that every Alert_Type gives the same yearly total, since
summing across types would count each placement six times; that exactly
two regions are fully observed; and that all three unobserved capacities
exceed the largest observed one, which is what makes this MNAR.
New outputs: 12_stock_flow.csv and 13_capacity_mnar.csv. The
capacity analyses need Ontario's institutional-locations CSV, a separate
CKAN dataset, shipped with the example and overridable with
OTIS_LOCATIONS.
adp(), alos(), admissions(), adp_from_counts() and
stock_flow() implement the measures from Lakner, A Manual of
Statistical Sampling Methods for Corrections Planners (University of
Illinois at Urbana-Champaign, 1976). One quantity, person-days, carries
two denominators:
stock_flow() puts both in one table with the decomposition, because
when length of stay moves the two can carry opposite signs and a
report quoting either alone states the wrong direction for the other.
The print method says so explicitly when the signs disagree.
This is not a hypothetical. On Ontario's segregation data the number of
people held fell 24.0% between FY2023 and FY2025 while their stays grew
43.5% longer, so total detention days rose 9.0% and the average daily
population rose with them. A flow rate falls 27.6% over exactly the
window in which a stock rate rises 3.9%. The decomposition is exact:
days are people times length of stay, so
(1 + p) * (1 + l) - 1 recovers the change in days.
admissions() inverts the identity when two of the three quantities are
published and the third is not. adp_from_counts() covers the case
where only periodic headcounts exist rather than a record per person
(Lakner eq 2.7, p.21), with the assumption stated: the days counted must
not differ systematically from the days missed.
alos()'s documentation carries Lakner's own caveat (p.16-17), which is
easy to lose: the period must exceed the longest stay people actually
serve, or the average is biased DOWNWARD, because the longest stays are
the ones that fail to finish inside the window.
Nothing about the arithmetic is specific to custody -- the same relationship governs hospital beds, shelter occupancy and open caseloads. It is Little's law under the names corrections planning uses.
The tests anchor on Lakner's own worked examples rather than on each other: 13,500 days over a year giving 36.986; 12,150 days across 2,700 people giving 4.5; 25 held daily against 1,750 admissions giving 5.2; and counts on 255 days summing to 34,935 giving 50,005 person-days. Those are numbers computed by someone else, so they can disagree with this code, which is the property a test needs.
A count is not comparable across places of different size or years of
different population, so rate() divides counts by exposure and scales
to a denominator -- per = 1000, "10k", "100k", "1m", or any
positive number. The interval is the exact Poisson one, matching
stats::poisson.test to the last digit, so a count of zero gives a
lower limit of exactly zero instead of a negative rate.
share() is the other quantity people mean by "percentage": what
fraction of a total each count is. Its denominator is the total of the
same events, not a population, so shares over a complete grouping sum
to 100. The interval is Wilson's rather than the normal approximation,
which runs past the ends of the scale and reports negative percentages
at exactly the small counts people reach for it.
Labelling one of these as the other is the most common error in a published table, which is why they are separate functions with separate intervals rather than one function with a flag.
rate_change() is the change in a rate between periods. This is not
the percent change of two rates treated as measured numbers: both
denominators move, and an interval that ignores them understates the
uncertainty. It conditions on the total of the two counts and corrects
for the exposure ratio -- yoy()'s exact conditional-binomial interval
generalised to unequal denominators, and it reduces to exactly that
interval when the two populations are equal, which is a test.
Like yoy(), the comparison period is matched on the period value and
not on row position, so a missing year reports no comparison instead of
silently comparing 2023 against 2020.
The otis-mrp example now recomputes every published OTIS
year-over-year table from the province's own CSVs and compares all of
them: 147 tables across 29 datasets, 8,214 cells. Previously the
example verified the models and the descriptives but said nothing about
those tables, so a reader checking them had nothing to check against.
The table definitions are a port of the generator that produced the published page, not a reimplementation from column names -- percent change rounded to one decimal and undefined when the earlier value is zero, shares against the column total, groups sorted with NA and empty dropped. Getting those subtly different would produce mismatches that look like data errors and are actually definition errors.
It also checks something a cell-by-cell comparison cannot: three OTIS
datasets reach the same restrictive-confinement population by different
routes, and a01 distinct individuals, c01 totals and c04 totals
must agree per fiscal year (20,781 / 19,641 / 25,045). A wrong grain
rule would move both sides of a cell comparison together and pass; this
fails.
The datasets are ~14 MB and are not shipped. They come from
OTIS_DATASETS_DIR if you already have them, or OTIS_YOY_DOWNLOAD=1
to fetch them from the province. rmoriedata is deliberately not a
source: its OTIS files are five-row samples for examples, so comparing
published totals against them would fail by construction rather than
tell anyone anything.
Files are named from the CKAN resource URL, which ends in the canonical
filename for all 29. Nothing is inferred from the resource title, which
would be guesswork -- "Segregation Placements - Maximum, Median and Mode
Consecutive Durations by Region" is the file called
b04_segregation_placements_consecutive_durations_by_region, and 15 of
the 29 diverge that way. A column signature then confirms a downloaded
file contains what its name claims, which catches the province
reshuffling data behind a URL -- an error no comparison against
published output could catch, because both sides would move together.
The example can verify the 108 published rate tables across 24 OTIS datasets, 15,831 cells, and does so on request:
OTIS_RATES_VERIFY=1 Rscript analysis.R <input> <outdir>
It is off by default, and not because it fails -- it passes all 108. It is held back because a group row in those tables divides that group's count by the WHOLE yearly population, so it states how much the group contributes to the overall rate rather than the rate among its own members: men's placements over the whole restrictive-confinement population is not a rate for men. c01 carries the population by gender, so a matched denominator exists for some tables and not others, and choosing per table is its own piece of work. The example already has the rates it needs, and the year-over-year tables already give counts, shares and percentage change for every published table.
Each count is expressed against two exposures:
c01, which states all three regime totals
directly. Those totals were checked against the detailed datasets
rather than trusted: a01 distinct individuals equal c01's
restrictive-confinement total, and b01 and b02 distinct
individuals equal its segregation total, in every year.This is the construction criminology uses for a rate -- a count over an
exposure, which in a count model enters as an offset of log(exposure).
Intervals are the exact Poisson interval, and change between years is
the exact conditional interval for a rate ratio corrected for both
exposures, cross-checked against the two-sample stats::poisson.test.
otis_headline_rates() reports the named rates. Over FY2023 to FY2025
the incarceration rate rose from 216.7 to 267.5 per 100,000 residents
while the solitary-confinement rate fell from 81.6 to 59.1, and within
custody solitary use fell from 376.7 to 221.0 per 1,000. The two move in
opposite directions, which a count alone hides.
The recomputation deliberately does not port the generator's
arithmetic, as the year-over-year check does; it goes through rate()
and rate_change(), whose intervals are anchored to base R. That
independence found two defects in the generator that a like-for-like
port would have reproduced silently: the rate-ratio arguments were
reversed, giving a +34.4% change an interval of -25.9 to -25.3 that did
not contain its own estimate, and a group with no exposure in a year got
an upper limit of Inf on a change that was simply undefined. Both are
now guarded by assertions that every rate and every change must lie
inside its own interval.
A group row in these tables is a CONTRIBUTION, not a per-capita rate: the numerator is that group's count and the denominator is the whole yearly population, so men's placements over the whole restrictive-confinement population is not a rate for men. Where the measure counts recurring events rather than people the per-1,000 figure can exceed 1,000, for the same reason one person can hold several placements. Both facts are stated on every table rather than left for a reader to infer.
The canonical glmmTMB fit returned a non-positive-definite Hessian and
no AIC, which the example flagged as a warning for several releases. The
cause is identifiable: the negative-binomial dispersion runs to
4.35e+08. A negative binomial whose theta goes to infinity IS a Poisson,
so the likelihood is flat in that direction and the Hessian is singular
in it. The rc random intercept, with a standard deviation of 4.5, has
already absorbed the overdispersion theta would explain -- the outcome
is 93% zeros with mean 0.173 and variance 0.473 -- so the two compete to
describe the same variation and one is left unidentified.
The fit now names the model it is actually fitting. The Poisson gives the same coefficient and the same standard error to four decimals, with a positive-definite Hessian and an AIC of 3045.3 against the published 3041.7. Which family produced the numbers is recorded in the manifest, because a coefficient is not interpretable without it.
A different optimiser was the wrong answer and was tried first: Nelder-Mead converges, but to a worse optimum (AIC 3054), trading a missing AIC for a wrong one.
The example analysis had an absolute path to the author's own volume as
its default input. It could not exist on a reviewer's machine and it
published a local directory layout, so the input is now searched for --
command line, then OTIS_INPUT, then known filenames in the working
directory and beside the script -- and the script stops with the exact
command to run when it finds nothing.
lme4 and DHARMa were attached and recorded as dependencies but
never called; both are gone. The header now also says which arm is
canonical for what: rmorie::morie_otis_irm_dml is the preferred DML
path and makes DoubleML, mlr3, mlr3learners and lgr
unnecessary, while MatchIt and glmmTMB stay because the published
numbers came from them and the script exists to let a reviewer check
those numbers.
The analysis now also writes 08_rates_and_yoy.csv: movement rates per
1,000 placements and per 1,000 person-years, each year's share of the
total, and the year-over-year change in the rate. Capsule bundles carry
yoy.R and rate.R so this works with nothing installed.
A capsule bundle sources json_native.R with no compiled library
present, so calling the registered native decimal converter failed
with object 'C_rmbl_strtod' not found and every bundle run died.
The converter is used when it is there and R's own reader when it is
not.
test-attest.R compared the decimal converter against decimal
literals, which are converted by whatever C library R was built
against -- the thing under test. On macOS both sides moved together
and the test failed against the correct answer. Expected values are
now transported as the bytes of the double.
The version-drift test read DESCRIPTION from a source tree that
R CMD check does not provide, erroring on all five platforms.
rate_change() on non-numeric periods indexed with positions that
could be zero, which returns a shorter vector and recycles wrong
answers rather than reporting anything.
analysis.R called say(), which is defined only in the bundle's
lib_interactive.R. Run from a bundle it resolved; run directly from
a checkout -- which is how a reviewer runs it -- it died with
Error in say(...). Both call sites use cat() now.
The example's default input was an absolute path inside the author's own volume. It could not exist on a reviewer's machine and it published a local directory layout. The input is searched for now.
Column signatures are compared after sorting with method = "radix".
The default sort() uses the locale's collation, which orders
Number_Of_Placements before NumberConsecutiveDays_Segregation
while byte order does the reverse; one dataset of the 29 differs only
in that pair, so under the default sort its signature failed to match
itself and the file went unidentified.
Certificate path validation is now complete: the three things 0.4.5 listed as still missing are done.
A CA can be limited to part of the name space, and a verifier that
ignores the limit treats a CA constrained to one organisation's domains
as able to issue for any name at all. cert_chain_verify() now enforces
nameConstraints for every CA in the path against every certificate
below it -- not only the leaf, so a constrained CA cannot escape by
issuing an intermediate.
The matching rules are per type and deliberately not shared: a DNS
constraint of example.org covers host.example.org and
example.org; an email constraint of example.org covers mailboxes
whose host is exactly that and NOT its subdomains; a directory name
constraint matches whole relative distinguished names, so O=Acme is
not satisfied by O=AcmeCorp. pathLenConstraint is enforced too.
certificatePolicies, policyMappings, policyConstraints and
inhibitAnyPolicy are processed as RFC 5280 section 6.1 describes, with
its three counters. Pass policies to cert_chain_verify() and the
path must yield one of them after mapping; omit it and policies are
still processed, but only reported as a failure where a certificate in
the path requires an explicit policy. Mapping to or from anyPolicy is
rejected, as the standard requires.
Not done: policy qualifier processing. A user notice attached to a policy is parsed past rather than surfaced.
revocation = "fetch" retrieves CRLs from the distribution points in
the certificates and queries any OCSP responder they name. OCSP is
POSTed as RFC 6960 requires a responder to accept, falling back to the
optional GET form.
It is opt-in, and that is the design rather than caution. A verifier
that reaches out during a check stops working offline -- which is where
an archival capsule is most likely to be verified -- becomes
non-deterministic, and tells whoever runs the responder which
certificates are being checked and when. "supplied" (the default)
uses only CRLs handed in; "none" skips revocation entirely.
A responder's answer is believed only when its signature verifies under a certificate in the path, or one it carries that the path issued. An unverifiable "good" is reported as a failure: treating it as a pass would be worse than skipping the check, because it would look like the check had happened.
C_rmbl_http_post(), so OCSP can POST. It is the only thing in the
package that sends a body.openssl verify on the
same certificates -- it rejects the three name-constrained leaves and
the wrong-policy leaf that these tests reject -- and the OCSP request
this package builds is byte-identical to openssl ocsp -reqout,
which checks the DER encoder, the CertID and the SHA-1 at once.logical(0) trap in policy processing: is.na(NULL) is
logical(0) and if on it is an error, so a counter read from a
structure that did not carry the field failed instead of defaulting.The four things the previous release documented as deliberately incomplete are now complete.
timestamp_verify() gained trust, crls and at_time, and there is
a new cert_parse() and cert_chain_verify() behind them. The chain is
built to an anchor you name, every signature in it is verified, every
validity window is checked, an issuer must be a CA, and the leaf must
carry the timeStamping extended key usage. A CRL can be handed in.
Validity is judged at the time the TOKEN asserts, not at the time the check runs. A token signed in 2020 under a certificate that expired in 2021 was validly signed, and judging it by today's date would reject it for a reason unconnected to its validity.
Omitting trust no longer passes quietly: certificate_trust is
reported as failed, because a signature that verifies under an
unvouched-for certificate says only that some key signed the token.
Still not done, and now the only gaps here: name constraints, policy mapping, and fetching revocation data over the network.
rmbl_ecdsa.cpp implements ECDSA verification over P-256, P-384 and
P-521 -- field and group arithmetic, Jacobian point operations, and the
FIPS 186-4 verification equation with the range and on-curve checks that
a lax verifier skips. Certificates and timestamp tokens signed with
ecdsa-with-SHA256/384/512 now verify; the test fixtures carry one
token of each kind over the same payload under the same CA, so the two
paths are exercised against real tokens rather than against each other.
SHA-384 was added for ecdsa-with-SHA384, checked against its FIPS
180-4 vectors.
One bug is worth recording because of how it presented. The P-521 group
order was written four hex digits short. Every published base-point
multiple still matched -- the curve arithmetic uses only the field
prime and b -- while every operation mod n was wrong and no signature
verified. The fix added a width check on every curve constant and a test
that n * G is the point at infinity, which is the property that fails
the moment the order is wrong.
capsule_power() injects an effect of known size, reruns the whole
detection procedure, and reports the rate at which it is found. The
smallest size detected reliably is the smallest effect the analysis
could have seen.
This is the case the negative controls cannot reach. A procedure with no
power against the effect at issue passes every control in
capsule_falsify() by failing to see anything at all, and a null result
from it is not evidence of absence. A design whose permutation floor sits
above alpha -- where no size could ever be detected -- is refused rather
than run.
Worst case went from 7.20 seconds to 2.67, and the SHA-2 sets from 7.20 to 1.09. Nothing is gated behind an environment variable any more; every parameter set signs in the test suite.
Three changes, in order of what they were worth:
% 5 on every lane -- the
permutation is straight-line code with literal indices, generated from
the formulas rather than transcribed;sha256_update() and sha512_update() also now copy in bulk rather
than a byte at a time, which speeds up every other user of them.
All 15 signature parameter sets remain byte-identical to OpenSSL 3.5, re-checked after the optimisation: 300 comparisons, no differences.
verify_capsule() checks a manifest against itself, which catches an
edited manifest and cannot catch one that was wrong when it was written.
manifest_recompute() re-runs named statistics against the data and
compares each to what was recorded. Results that were recorded but
NOT recomputed are reported as unchecked: an analysis that recorded
twenty statistics and re-derives three has seventeen it has not, and a
report that quietly omitted them would read as a clean bill of health.manifest_record_seed() and manifest_restore_seed() record and
replay the generator's full state. set.seed() is reproducible only
if everything before it is too -- one extra draw upstream shifts
every later value -- so the state is what gets recorded, and the kind
beside it, because a seed replayed under a different kind gives
different numbers silently.capture_dependencies() records where each package came from: the
library, the repository, and for a remote install its URL and commit.
Two installations can report the same version and differ.prereg_declare() and prereg_check() -- declare the statistics an
analysis intends to report, and compare that against what it did.
The two departures are reported separately because they are different
failures: a declared statistic that was not reported is outcome
switching, and a reported statistic that was not declared is an
addition. Both are invisible without a declaration made in advance.falsify_family() -- corrects a family of permutation p-values by
Holm, Benjamini-Hochberg or Bonferroni, and names those sitting at
the permutation floor, where more permutations would be needed to say
anything more. Running the control over twenty statistics and
reporting the one under 0.05 is not a finding.evalue_rr() -- the E-value of VanderWeele and Ding: how strong an
unmeasured confounder would have to be, with both the exposure and
the outcome, to explain the result away.capsule_bundle(), capsule_bundle_read() and
capsule_bundle_verify() -- one signed artifact holding a digest of
every file, the manifest digest and an attestation over both. The file
digests are inside the signature: a list of hashes that is not itself
signed can be rewritten to match whatever the files now say. Files
present but unlisted are reported too.
timestamp_verify() and timestamp_info() -- RFC 3161 timestamp
tokens, verified natively. The message imprint is checked against the
data, the genTime is reported, and the authority's RSA signature over
the signed attributes is verified, which meant implementing DER
parsing and a bignum modular exponentiation (C_rmbl_der_parse,
C_rmbl_rsa_recover). A hash chain proves the order of a sequence of
manifests and not that any of them existed at a given time; this
supplies the date.
What it does NOT do, stated plainly because it is the difference between this and a browser's padlock: validate the certificate. No chain building, no validity dates, no revocation, no check of the timeStamping key usage. Pass the certificate you have decided to trust, and read a pass as "this key said so".
write_manifest_json() wrote doubles at four significant digits, so a
manifest recording 1/3 said 0.3333 and no later recomputation could
match what was written. Every number is now written at full double
precision -- seventeen significant digits, which is enough to recover
any double exactly -- and round-trips exactly, the smallest denormal
included.
That holds on every platform, because the package no longer asks the
platform. Seventeen digits recover any double only through a reader
that rounds correctly, and not every C library does: macOS arm64
(R 4.6.0) reads the correct decimal for .Machine$double.xmax as
Inf, and loses low bits above about 1e100 on text written elsewhere.
So the decimal conversion is done here instead, in integer arithmetic
with a remainder that decides the rounding -- round to nearest, ties to
even, with no floating point involved in the decision. A manifest
written on one machine now reads back bit-identically on another, and a
caller does nothing to get that. A provenance record that cannot reproduce its own numbers is
the one failure mode the whole capsule apparatus exists to prevent, and
this was it.
manifest_canonical() and manifest_digest() -- one line of JSON
with every object's keys sorted, and its SHA-256. R lists keep
insertion order, so the same manifest assembled in a different order
used to serialise to different bytes, which made a signature over the
JSON depend on the order a script happened to build a list in. Sign
the digest.capture_environment() now records rng_kind and whether a seed was
in force. Without the generator's identity a stochastic result cannot
be reproduced even on the same machine: R has changed its default
sample() algorithm before, and a recorded seed means nothing without
the kind it was fed to.capsule_attest() and capsule_check_attestation(). A bare signature
leaves three things implicit -- which key, which scheme, which bytes --
and a verifier who has to be told them out of band cannot check anything
they were not already given. An attestation records the scheme, the
public key, the context, the pre-hash, the manifest digest and a note
from the signer, all inside the signed payload, so the check is
capsule_check_attestation(attestation, manifest) and nothing else.
Editing any field afterwards is detected, including the note.
capsule_falsify() runs four negative controls against a statistic and
reports whether it behaved: a permutation test that destroys the
association on purpose, a random common cause that cannot matter, a
placebo exposure, and subset stability. Reproducibility is a property of
a pipeline, not of a claim -- a capsule can be signed, hashed, chained
and reproduced byte for byte while reporting a number that means
nothing.
The permutation control reports the smallest p-value its design could have produced, because a reader who does not know that 19 permutations floor at 0.05 will over-read a p of 0.05. The controls are demonstrated failing as well as passing: a constant statistic fails the permutation test, noise fails it, and a statistic that reads the injected noise column fails the random-common-cause control.
kem_keygen(),
kem_encapsulate(), kem_decapsulate(), kem_sizes(). A key
encapsulation mechanism answers a different question from a
signature: not who produced a capsule but what key two parties now
share. Decapsulation has no failure path, deliberately: a bad
ciphertext yields a shared secret derived from a value held only
inside the decapsulation key, so the sender learns nothing from
whether it worked. That is the Fujisaki-Okamoto transform's implicit
rejection.capsule_sign(prehash = ), for signing a digest of the
message rather than the message. The identifier of the pre-hash is
bound into the signature, not merely its output, so a signature over a
SHA-256 digest is never interchangeable with one over a SHAKE128
digest of the same length.fips_mu(), fips_sign_mu(),
fips_verify_mu(). A message can be reduced to the 64-byte value that
is all ML-DSA signing consumes, and only that handed to whatever holds
the key, so a large file never has to cross the boundary the key sits
behind. mu binds the public key and the context, so it is not a bare
digest.Every one of these is checked against OpenSSL 3.5, which shares no code with this package. ML-KEM keys generated from the same seed agree byte for byte at all three levels, its ciphertexts decapsulate here to the secret it reports and ours to the secret we report, and a corrupted ciphertext produces the same rejection secret in both. The pre-hash and external-mu signatures are byte-identical for every parameter set and every pre-hash.
ML-DSA (FIPS 204) and SLH-DSA (FIPS 205) were previously reached through
liboqs, so which schemes a build offered depended on what happened to be
installed on the machine that built it -- a poor property for a signature
format meant to outlive that machine. Both are now implemented in the
package, on its own Keccak sponge, and the optional system dependency is
gone along with the ./configure step that probed for it.
fips_keygen(), fips_public_key(), fips_key() and fips_sizes()
are the new entry points. pqc_backends() now reports a fixed list --
ML-DSA at all three parameter sets and SLH-DSA at all twelve, six over
SHAKE and six over SHA-2 -- available in every build. fips_key()
wraps key material from elsewhere, so a key written by another
implementation can be used directly.capsule_sign() and capsule_verify() gained context, the context
string both standards bind into the message encoding, so one key can
be used for two purposes without a signature crossing between them.
capsule_sign() also gained deterministic, selecting the variant
whose output depends only on key, message and context.oqs_keygen() and oqs_public_key() are deprecated in favour of the
fips_* names and now warn. They still work, and still return the
same classes.Every parameter set is checked against OpenSSL 3.5, which shares no code with this package: in deterministic mode the two produce the SAME BYTES, over several message and context lengths, and each verifies the other's signatures. That cross-check is the conformance claim. Reference parity is not: this implementation matched the pq-crystals and sphincsplus reference code byte for byte while disagreeing with the standards twice over -- FIPS 204 and FIPS 205 both prepend a context domain separator the reference code omits, and FIPS 205 reads the FORS indices most significant bit first where SPHINCS+ read them least significant bit first. Neither is detectable from the inside: a signature scheme that verifies only its own output still rejects every tampering.
The release that makes a capsule answer three questions a checksum cannot: is this still the same data, who says so, and what is actually in it.
A SHA-256 tells you the bytes changed. It cannot tell you whether the distribution changed -- and those are different questions. A re-released open-data extract legitimately has a different digest while being the same data statistically; conversely a column can keep its name, its type and its row count while having been silently rescaled, and no digest notices.
capsule_drift() tests every shared column between a pinned extract
and a fresh fetch, and reports which moved.drift_ks() (two-sample Kolmogorov-Smirnov), drift_psi()
(population stability index and Jensen-Shannon divergence),
drift_homogeneity() (two-sample chi-square) and drift_chisq()
(goodness-of-fit against a known distribution) are available
individually.benford_test() screens a numeric column's leading digits against
Benford's law -- a cheap check on figures that were rounded, capped or
invented. It is a screen, not a verdict, and says so.capsule_drift() uses the two-sample homogeneity test for categorical
columns, not goodness-of-fit: the reference is itself a finite sample,
and treating it as a known distribution ignores its sampling error and
reports drift too readily. For the same reason the population stability
index is reported as an effect size but only allowed to raise the drift
flag once both samples pass psi_min_n -- its 0.25 "material shift"
band is a large-sample heuristic with no calibrated null, and on a few
hundred rows binning noise alone clears it.
A digest in a manifest proves the data was not corrupted. It proves nothing about who produced it, because anyone who edits the data can recompute the digest.
capsule_sign() / capsule_verify() authenticate a manifest, either
with a shared secret (HMAC-SHA-256, RFC 2104) or with a
post-quantum, asymmetric, hash-based signature.pqc_keygen() builds that signature's key: a Merkle tree over
Winternitz one-time keys, following the RFC 8391 construction over the
SHA-256 this package already ships. Its security rests on the hash
alone -- no lattice assumption, no elliptic curve, nothing Shor's
algorithm breaks, and no new system dependency.merkle_root() and friends pin a capsule chunk by chunk, so a
mismatch names which chunk moved rather than only that the file did.chain_new() / chain_append() link each manifest to the digest of
the one before, making the run history tamper-evident. chain_seal()
is the value to sign: the head alone misses a deletion from the middle
and the links alone miss a truncation from the end.On post-quantum choices: SHA-2 and HMAC are already adequate against a quantum adversary, since Grover only halves the exponent. Signatures are the part Shor breaks, so that is the part replaced.
A lattice scheme is deliberately not hand-rolled here -- an
uncertified hand-written NTT and rejection sampler would be a worse
outcome than no lattice signature. Instead, ./configure looks for
liboqs, and where it is found oqs_keygen() exposes the standardised
schemes -- ML-DSA (FIPS 204) and SLH-DSA (FIPS 205) -- computed entirely
by that library. pqc_backends() reports what the build actually
enabled, checked per scheme, since liboqs is configurable. Absence is
not an error: the package builds without it, and the bundled hash-based
scheme needs nothing.
Unlike the hash-based key, the standardised keys are STATELESS -- one key signs any number of messages, with no leaf index to track. A signature is never verified against a key of a different scheme.
A height-h signing key signs exactly 2^h messages. Signing twice at
one index breaks the scheme outright, so capsule_sign() tracks the
index, returns the advanced key state, and refuses an exhausted key
rather than wrapping around.
A capsule is only worth pinning if somebody looked at the data first, and the look is the step that gets skipped.
profile_columns() describes every column: type, missingness,
distinct values, the counts of zero, negative and infinite values, the
classical and robust centre and spread side by side (where they
disagree, the mean is not describing the column), and an
inline_hist() sketch that shows bimodality no summary number
carries.frequency_table(), correlation_table(), top_correlations(),
duplicate_rows(), drop_empty(), drop_constant() and
clean_column_names() cover the rest of a first pass.mahalanobis_outliers() finds rows that are unremarkable on every
variable separately and impossible jointly. It defaults to a robust
centre and scale, because outliers inflate the very covariance used to
judge them.missingness_summary(), missingness_pattern(), missing_runs() and
missingness_map() describe the shape of the gaps, not just the
rate: whether columns are missing together, and whether a gap is one
outage or scattered failures.mcar_test() is Little's test for missing-completely-at-random --
the assumption that licenses dropping incomplete rows. It carries an
expectation-maximisation estimator, because the maximum-likelihood
mean and covariance under missingness have no closed form; on complete
data that estimator reproduces the ML estimates exactly.environment_diff() reports what moved between two runs' captured
environments, which is the question a failed reproduction actually
raises.capsule_report() runs the checks above over one data frame and collects
the findings into a single report, ordered by severity: a missing
required column is fatal, a drifted column or an unverified signature
is a warn, an outlier or a Benford departure is a note. It exists
because the failure mode this package is built against is a person
running one check, seeing it pass, and concluding the data is fine.
Each problem is reported ONCE. An entirely missing column is missing, is trivially constant, and makes any covariance singular; reporting all three would bury the findings that matter under a single cause. The collinearity note therefore fires only for a genuinely duplicated or derived column, which is the case that tells the reader something new.
report_markdown() writes the assessment beside the capsule it
describes. Both renderings carry the caveat that no finding is proof of
correctness.
infer_schema() derives the schema validate_schema() consumes from
data you already trust, so a capsule need not be pinned on nothing.
It describes one extract and should be read and edited, not trusted
blindly.validate_schema() gains the checks to match: column types (with
integer and double treated as interchangeable), numeric ranges, and
per-column missingness ceilings. Every field stays optional, so
schemas written for earlier versions are unaffected.rule() and validate_rules() express the project-specific checks a
generic schema cannot -- an age that must be non-negative, two dates
that must be ordered -- and rule_in_set(), rule_between(),
rule_not_null(), rule_unique(), rule_regex(),
rule_increasing(), rule_within_n_mads(), rule_complete_rows(),
rule_distinct_rows() and rule_col_count() supply the common ones
ready-made.online_summary() accumulates mean, variance, skewness and kurtosis
block by block. The merge is exact -- Chan, Golub and LeVeque's
parallel combination with Terriberry's higher moments -- so a chunked
pass agrees with a single batch pass rather than approximating it.reservoir_indices() / reservoir_sample() take a uniform sample of
a stream in one pass (Vitter's Algorithm R).distinct_sketch() / distinct_count() estimate cardinality in fixed
memory (HyperLogLog), with the published relative error.Several things existed in the package with no way to call them. The compiled core carried kernels that had no binding, and the JSON codec had three internal helpers:
core_sd(), core_dist(), core_normal_logpdf(),
core_bootstrap_mean(), core_ipw_weights(), core_gamma_cdf() and
core_hawkes_nll() now reach kernels that were already compiled in.bricklayer_json_serialize() / bricklayer_json_unserialize() --
lossless JSON that round trips an object rather than only its data --
were internal, and left a dangling documentation link.bricklayer_json_base64_enc() and the URL-safe
variant) was internal too.json_gzip_encode() / json_gzip_decode() are new: gzip plus base64,
for a payload that travels.All of these are published through LinkingTo: rmoriebricklayer, so
rmorie and rmoriedata call one compiled copy rather than carrying
their own.
pqc_keygen()'s seeds previously came from a function that mixed the
clock, the process id and R's Mersenne Twister. set.seed() makes R's
generator reproducible by design and its state is recoverable from its
output, so a key drawn from it is guessable.
random_bytes() reads the operating system's CSPRNG and fails
rather than falling back to a weaker source.derive_key() is PBKDF2-HMAC-SHA256, so a passphrase can stand in for
raw key bytes.core_blake2b() is a natively keyed digest of any width from 1 to 64
bytes -- a MAC without the HMAC construction.digest_object() fingerprints an arbitrary R object.Three functions now REFUSE input they previously computed through. In each case the old answer was a number produced by an internal guard rather than by the data, which is worse than an error because it looks like a result.
mahalanobis_outliers() and mcar_test() reject exactly collinear or
constant columns. Both repair a singular covariance by flooring its
eigenvalues so the algorithm can proceed; with a duplicated column
that floor, not the data, determined the answer. mcar_test() checks
the complete-case covariance where it can, because two identical
columns with different missingness leave the pairwise covariance only
nearly singular.capsule_verify() rejects a signature presented with a key of a
different scheme explicitly. It already failed, but incidentally, on a
length mismatch.Four defects in the report parser, each of which produced a wrong field rather than an error:
Section 320.13, Criminal Code -- the ordinary dangerous-driving citation -- never
reached relevant_legislation;Dated at Toronto.\n\nAlex Morrow\nDirector yielded "Toronto. Alex Morrow", and a force named
once just under a heading picked the heading up with it;&, and one smart quote were decoded, so <,
>, ", ', the remaining quotes and the dashes
survived into the extracted text.An open-data extract from a criminal-justice system has a shape the general-purpose toolkits do not assume: a handful of fiscal years, counts rather than measurements, categories reported as bands rather than values, and a region code with no geometry attached.
yoy() computes period-over-period change and declines it in the three
cases where the figure would describe something other than the data.
Periods are matched on their own value, not on row position, so a
missing year is a gap rather than a quietly multi-year comparison. A
percent off a base below the gate is withheld with its reason recorded
-- two placements becoming twenty is a 900% rise and also nothing at all
-- while the count change and the direction are still reported, those
being facts. A column already in percent is handled in percentage
points, a percent of a percent being a different quantity. For
counts the interval is exact: conditional on the two periods' total the
current count is binomial, so the ratio has a Clopper-Pearson interval,
verified identical to stats::poisson.test on every case including both
zero boundaries.
yoy_write() renders to HTML, PDF, CSV, TSV, JSON or Markdown, with
the format taken from the file name; yoy_html(), yoy_pdf(),
yoy_csv(), yoy_tsv(), yoy_json() and yoy_markdown() are
available individually. Nothing here needs a package beyond base R:
the HTML is self-contained and makes no network request, so it renders
later as it rendered when the capsule was sealed, and the PDF is drawn
on R's own device and paginates rather than truncating.yoy_summary() gives the change across the whole span and the
compound rate per period. yoy_label() and fiscal_year_label()
render a fiscal year by the years it spans, so an EndFiscalYear of
2023 prints as 2022/23 rather than naming a calendar year the row is
not about.yoy_palettes() offers a
colour-blind-safe pair and a monochrome option for print.parse_bands() reads the interval labels publishers actually use --
"2 to 5", "50+", "Greater than 10", "under 18", and the whole
dash family -- and returns bounds, distinguishing the inclusive
wordings from the exclusive ones: "65 and over" starts at 65 and
"over 65" at 66, which is a whole unit of the quantity being measured.
band_values() makes the representative-value rule explicit, and
marks the rows whose value rests on an assumption about the open top
band, that band having no midpoint to take.band_sensitivity() reports how far a derived statistic moves as the
assumed cap varies. Everything computed from banded data carries that
dependence; the only question is whether it was measured.gini(), lorenz() and top_share() answer whether a few units
account for most of a total. Gini is verified both against the
mean-absolute-difference definition and against twice the area under
its own Lorenz curve. Its maximum for n units is 1 - 1/n, not 1,
so the value means different things across ten units and ten thousand.hill_tail_index() maximises the exact discrete likelihood. The
closed-form continuity correction usually quoted is an asymptotic
approximation in the threshold, and at a threshold of one -- where
administrative counts start -- it returns about 2.0 from data
generated with an exponent of 2.5. It also reports a
goodness-of-fit distance, because an exponent fitted to a tail that is
not a power law is a number with no referent.hurwitz_zeta() is the normalising constant that needs, computed by
Euler-Maclaurin and verified against pi^2/6, pi^4/90, Apery's
constant and the shift identity.cramers_v() carries Bergsma's bias correction, without which a
sparse table reports association that is an artefact of its size, and
permutes its p-value rather than trusting the chi-square
approximation when an expected count is small.trend_test() is Mann-Kendall with a Theil-Sen slope: no
distributional assumption, resistant to one aberrant period, exact by
enumeration up to eight periods, and agreeing with
stats::cor.test's tau and exact p-values.step_change() scans every admissible split and takes its p-value
from the permutation distribution of the maximum over splits, not
from the best split's own test -- which is how a break is found in any
series. It records that six points cannot produce a p-value below
(1 + 72) / (1 + 720) however clean the step.count_trend() is a Poisson rate ratio per period, matching
stats::glm to 1e-8, with an offset for a varying denominator and a
widening to quasi-Poisson when the dispersion says the Poisson
interval is too narrow.A region is an areal unit with a population, not a coordinate.
expected_counts() does indirect standardisation, removing the part
of a difference explained by who the area holds. The expected counts
total the observed ones, which is the identity that makes them a
standardisation rather than a prediction.sir() reports the ratio with the exact Poisson interval, identical
to stats::poisson.test's, including a lower limit of exactly zero at
an observed count of zero.eb_rates() is the Clayton-Kaldor empirical Bayes shrinkage, which
stops a small area's noise from ranking it to the top or bottom of a
league table. Where the between-area variance estimate is not
positive there is no evidence of real variation and every estimate
collapses to the overall rate, reported through total shrinkage rather
than hidden.funnel_limits() gives exact Poisson control limits, which stay
correct at the small expected counts where a normal funnel's lower
limit goes below zero.morans_i() requires a neighbour list and does not invent one, an
extract keyed on a region shipping no geometry. Its null expectation
is -1/(n - 1), not zero.Two vignettes cover the lot: Year-over-year change, and the three ways it goes wrong and Statistics for a published administrative table.
Every hash, keyed hash, checksum, key derivation, base64 and JSON output
is now compared against an independent implementation -- digest,
openssl, jsonlite and base R's own inflater -- over a length sweep
crossing each construction's block boundaries. Published vectors prove a
primitive reproduces a handful of documented inputs; they do not prove
it agrees with the libraries already in a user's pipeline. SHA-256,
SHA-512, HMAC-SHA-256, BLAKE2b, PBKDF2 and the JSON writer were already
exact. Five things were not.
strlen, and chunk_file() called
rawToChar(), which errors on any file containing a zero byte. A raw
vector handed in was coerced, so merkle_root(list(charToRaw("a")))
hashed deparsed text and returned a confident digest of the wrong
thing. Chunks are now bytes end to end, and chunk_file() returns a
list of raw vectors. Roots for character input are unchanged, so
manifests already recorded still verify.json_gzip_encode() did not produce gzip.
memCompress(type = "gzip") emits a zlib stream (RFC 1950, header
0x78 0x9c), not a gzip member (RFC 1952, 0x1f 0x8b), and
memDecompress reads both -- so the round trip looked correct while no
external tool could read bytes the documentation calls "raw gzip
bytes, for writing to a file". Now a real member, with the CRC-32 and
length trailer, and MTIME pinned to zero so a capsule digest does not
move with the clock.jsonlite
and re-encoded here differed, and a digest comparison read that as
drift. Now exact over every length from 0 to 400.digest_object() accepted key but wired it only to blake2b, so a
keyed sha256, sha512 or crc32 silently returned the unkeyed
digest -- an authentication request answered with a checksum. sha256
now routes to HMAC-SHA-256 and the two algorithms with no keyed form
refuse the key.json_gzip_decode() trusted its flag over its input's type, so
handing back what json_gzip_encode(raw = TRUE) returned, without
repeating the flag, ran the bytes through a base64 decode and failed
inside the inflater.gini, top_share, lorenz, mann_kendall, theil_sen and
hurwitz_zeta are published with R_RegisterCCallable and shimmed in
inst/include/rmoriebricklayer.h, so a sibling package reaches them
without going back through R.
A consumer package is now built and run against the header as part
of the suite. The two halves fail independently: a signature mismatch is
a compile error, while a wrong name in R_RegisterCCallable compiles
perfectly and raises only on first call, R_GetCCallable resolving
lazily. That test immediately earned its place by recording a real
requirement -- LinkingTo alone is not enough. It puts the header on
the include path at compile time but does not load the providing
package, so a consumer needs Imports: rmoriebricklayer as well or the
first kernel call raises "function 'rmbl_gini' not provided by package
'rmoriebricklayer'".
Everything with a published test vector is checked against it: SHA-512 against FIPS 180-4, HMAC-SHA-256 against RFC 4231, PBKDF2-HMAC-SHA256 against the published vectors including the multi-block case, BLAKE2b against RFC 7693, and CRC-32 against the ITU V.42 check value. The statistics are anchored on base R, the Merkle construction on digests recomputed by hand, and the exceedance and reservoir distributions on exhaustive enumeration and simulation respectively.
The XMSS signature scheme is byte-compatible with the RFC 8391 reference implementation. Getting there required fixing three divergences from the specification, none of which the previous security-property tests could detect, because a sound-but-wrong pseudorandom function passes every one of them:
PRF(SK_SEED, toByte(i, 32)) rather
than PRF_keygen(SK_SEED, PUB_SEED || ADRS) (NIST SP 800-208's
domain 4);PUB_SEED, with the index and root
pushed into the message, rather than keyed with
R || root || toByte(idx, n) as section 4.1.9 requires;SK_PRF, so the per-signature randomiser R that the
RFC binds the digest to did not exist at all.The whole 2500-byte signature for XMSS-SHA2_10_256 -- index, randomiser, WOTS+ signature and authentication path -- now matches the reference byte for byte, and the vectors are embedded in the test suite so the check needs neither a network nor a C toolchain.
This changes the key and signature formats. pqc_keygen() now
generates a third secret (sk_prf) and capsule_sign() returns
randomizer and wire alongside the existing fields. A key made by an
earlier build cannot sign, and a signature made by one cannot be
verified; both raise an explicit error rather than failing quietly.
Nothing is lost in practice, because signing is new in this release and
was never in a version on CRAN.
It is verified against its security properties as well -- a valid signature verifies, and every tampering of the message, signature, authentication path, index or key fails -- and it must still not be treated as certified, which is a statement about process rather than about the bytes. The standardised schemes carry no such caveat, because they are liboqs's implementation rather than one of ours; what is tested here is the binding, including that ML-DSA-65 produces the key and signature sizes FIPS 204 specifies.
Everything added in this release is anchored outside itself. The exact
count interval is checked against stats::poisson.test, the Poisson
trend against stats::glm, Kendall's tau and its exact p-values against
stats::cor.test, the funnel limits against stats::qpois, Gini
against its own definition and its own Lorenz curve, the Hurwitz zeta
against three published constants and a functional identity, and the
tail-index estimator against samples whose exponent is known by
construction -- the only anchor that can fail, since comparing against
the closed form would compare against the thing being replaced. The
statistical methods are grounded in the local corpus where it has them
(Lawson on standardised incidence ratios and Clayton-Kaldor shrinkage;
Hedderich and Sachs on the Lorenz construction and the indirect/direct
distinction); Bergsma and Clauset-Shalizi-Newman are not in it and are
cited from the published papers, marked as such.
Three source-level invariants are now checked too, because they break
only on platforms the development host is not: that no R header precedes
a standard header reaching libc++'s <locale> (R's length macro
otherwise rewrites it, which fails on macOS and is silent on Linux),
that every C entry point is registered with a matching arity and every
registration resolves, and that no source file carries a non-ASCII byte.
The suite is 5,300 assertions at 97.6% coverage, and
R CMD check --as-cran is clean.
Test fix; no code changes.
test-json-branches.R asserted one platform's spelling of a
full-precision double. With digits = NULL the encoder calls
sprintf("%.17g", ...), which asks the C library for 17 significant
digits and lets it choose how to print them: x86 gives
9.9999999999999995e-21 for 1e-20, Windows arm64 gives 1e-20. Both
are the same double and both round trip, but the hard-coded expectation
failed the arm64 builds on r-universe.
The test now asserts the contract the option actually promises -- full precision that survives a round trip -- and carries an anchor that fails if the round trip stops being exact.
Documentation only; no code changes.
The manual still credited 'jsonlite' and 'digest' for work the package now does itself, which had been true before 0.3.8 moved both to Suggests behind a native JSON codec and a compiled SHA-256 core:
load_provenance() said it parsed via jsonlite::fromJSON(), and
linked to it; it reads with bricklayer_json_from_json().verify_capsule()'s example called jsonlite::write_json() -- a
Suggests package used unconditionally in an example. It now writes with
bricklayer_json_to_json().sha256_file() and verify_sha256() said they hashed "via the digest
package"; they use the compiled core, or the bundled pure-R FIPS 180-4
implementation when a capsule is sourced standalone.wayback_snapshot_url_native() described the R-level resolver it
supersedes as "jsonlite-based"; that resolver parses natively too.write_manifest_json()'s example said it round-tripped through
'jsonlite'.toJSON()
option (dataframe/matrix/Date/POSIXt/factor/complex/raw/null/na/digits/
pretty/force plus rownames, keep_vec_names, json_verbatim, always_decimal,
time_format, UTC, no_dots), the same number formatting (num_to_char /
modp_dtoa2 rules), fromJSON() simplification (record lists, matrices,
arrays, $date, _row, "NA" strings), prettify/minify (yajl layout),
validate, serializeJSON/unserializeJSON, base64, ndjson streaming and
rbind_pages. A parity test pins all of it to jsonlite byte-for-byte when
jsonlite is installed (1,742 cases, 0 differences).make_bundle.sh now ships json_native.R
and a pure-R SHA-256 (sha256_native.R), setup_and_run.R no longer
requires jsonlite, and sha256_file() falls back to the pure-R digest
when the compiled core is not loaded. This fixes the 0.3.8 CI failure of
the otis-mrp bundle (bricklayer_json_from_json not found).stats, utils). sha256_file() hashes through the
compiled SHA-256 core that already backed core_sha256(); every JSON
read (CKAN, Socrata, ArcGIS, Wayback metadata, local manifests) and the
manifest writer go through a new pure-R, jsonlite-compatible codec
exported as bricklayer_json_from_json() / bricklayer_json_to_json()
(same simplification rules and encoder options as jsonlite). Both old
packages move to Suggests and are only used by the cross-check tests,
which pin the native codec and hash to their output when installed.bricklayer_fetch() (the
compiled fetch core with its Wayback fallback) instead of jsonlite's
URL reader.src/ (zero new dependencies, hand-rolled .Call glue like the rest of the backend). New: bricklayer_parse_siu() (16 schema fields + language from report HTML or a saved file), bricklayer_fetch_parse_siu() (fetch + parse in one call), bricklayer_siu_schema(), bricklayer_siu_text(), bricklayer_siu_iso_date(), bricklayer_siu_resolve_so() (rule-ordered subject-official count; 0 is a real answer). Synthetic-report fixture + offline tests included.\dontrun -> \donttest; setup_and_run.R no longer calls setwd(), install.packages(), or installed.packages() (checks via requireNamespace() and prints the install command instead).bricklayer_fetch_siu(drid, dest): fetch an Ontario SIU director's report by drid through the live+Wayback engine -- the fetch step of the open SIU corpus pipeline.bricklayer_fetch() help topic (added in an
earlier 0.3.1 build without a version bump) propagates to the
r-universe binary and downstream reverse-dependency checks.License wording corrected: the optional rmorie CLI that
agent_bundle() forwards to is AGPL-3.0-or-later (the entire MORIE
family is AGPL; an earlier internal comment mislabelled it
proprietary).
Package moved from the bricklayer/ subdirectory to the repository
root (required by rOpenSci's review tooling); repo-level extras stay
as .Rbuildignored siblings.
New vignette capsules.Rmd walking the essential flow offline:
provenance pin -> schema validation -> SHA256 integrity ->
synthetic fallback -> manifest + summary.
Every exported function now has runnable @examples (network calls
in \donttest; offline NULL-contracts shown runnable).
README gained development-version install instructions.
New CI: test coverage (covr + Codecov) and rOpenSci pkgcheck.
Test coverage raised from 84 percent to 96 percent: offline tests for the
CKAN/Socrata/ArcGIS resolver success paths, friendly_download
diagnostics and Wayback retry, summary contact/licence blocks, pinned
script hashes, and the multi-block SHA-256 path. Dead requireNamespace
guards for Imports (digest, jsonlite) removed.
Repo-level LICENSE text excluded from the build (License: AGPL-3
is the canonical spec; the stray file triggered a check NOTE).
verify_capsule(): one call re-verifies an entire reproducible
data capsule offline: provenance readability, pinned sha256/size/row
count, schema validity, script hash, and re-derivation of every
stored manifest cross-check from its own numbers.capture_environment() records R version, platform, OS, UTC
timestamp, and loaded package versions; make_manifest() now attaches
it by default (environment = FALSE to opt out).cite_capsule() generates a ready-to-paste data citation (text +
BibTeX @misc, DOI-aware) from a provenance object.resolve_via_socrata() and resolve_via_arcgis() extend
URL-rot recovery beyond CKAN to the Socrata (Calgary/Chicago/NYC) and
ArcGIS FeatureServer (Toronto Police Service) portals the MORIE
family fetches from.make_synthetic_column("id_pattern") without a year_col now returns
all n ids (a vectorised-gsub misuse returned a single id).bernoulli columns unlist JSON-derived labels, fixing mangled
column names from provenance-parsed schemas.row_replication no longer trips base R's scalar
sample() expansion.make_synthetic_csv() now restores the caller's .Random.seed on exit
(CRAN policy: no lasting RNG-state change).cran-comments.md, build exclusions.examples/otis-mrp/.