rmoriebricklayer 0.5.0

Point locations and the regions that contain them

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.

The otis-mrp example

Verification that runs

Printing

Bug fixes

rmoriebricklayer 0.4.9

Bug fixes

rmoriebricklayer 0.4.8

Why the version moved

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.

The stock and flow measures reach the example, and so does the MNAR pair

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:

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.

Stock and flow: person-days have two denominators

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.

Rates, shares and rate change

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.

Verifying the published year-over-year tables

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.

Rate tables, verified against both exposures – opt-in

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:

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 count model no longer reports a missing AIC

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 otis-mrp example

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.

Fixes

rmoriebricklayer 0.4.6

Certificate path validation is now complete: the three things 0.4.5 listed as still missing are done.

Name constraints

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.

Certificate policies

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 can now be fetched

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.

Along the way

rmoriebricklayer 0.4.5

The four things the previous release documented as deliberately incomplete are now complete.

Certificates are validated, not just used

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.

ECDSA, not only RSA

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.

Falsification can now confirm as well as refute

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.

Signing an SLH-DSA s parameter set is five to seven times faster

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:

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.

rmoriebricklayer 0.4.4

From “the record is intact” to “the record is right”

verify_capsule() checks a manifest against itself, which catches an edited manifest and cannot catch one that was wrong when it was written.

Falsification, and what comes before and after it

Distribution and time

rmoriebricklayer 0.4.3

A manifest can now reproduce its own numbers

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.

Attestation: a signature a third party can actually check

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.

Falsification: controls that can fail

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.

rmoriebricklayer 0.4.2

The rest of the NIST post-quantum standards

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.

rmoriebricklayer 0.4.1

The standardised post-quantum schemes are now implemented here

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.

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.

rmoriebricklayer 0.4.0

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.

Is this still the same data?

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() 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.

Who says so?

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.

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.

What is actually in it?

A capsule is only worth pinning if somebody looked at the data first, and the look is the step that gets skipped.

One command for the whole assessment

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.

Schemas and rules

Capsules larger than memory

Newly reachable

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:

All of these are published through LinkingTo: rmoriebricklayer, so rmorie and rmoriedata call one compiled copy rather than carrying their own.

Keys

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.

Behaviour changes worth knowing about

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.

SIU parser fixes

Four defects in the report parser, each of which produced a wrong field rather than an error:

Statistics for the tables these capsules hold

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.

Categories published as intervals

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.

Concentration, association and heavy tails

Trend in a series of a few periods

Region-coded counts

A region is an areal unit with a population, not a coordinate.

Two vignettes cover the lot: Year-over-year change, and the three ways it goes wrong and Statistics for a published administrative table.

Byte compatibility

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.

Newly reachable through LinkingTo

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’”.

Verification

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:

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.

rmoriebricklayer 0.3.11

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.

rmoriebricklayer 0.3.10

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:

rmoriebricklayer 0.3.9

rmoriebricklayer 0.3.8

rmoriebricklayer 0.3.7

rmoriebricklayer 0.3.6

rmoriebricklayer 0.3.5

rmoriebricklayer 0.3.4

rmoriebricklayer 0.3.3

rmoriebricklayer 0.3.2

Documentation

rmoriebricklayer 0.3.1

rOpenSci submission preparation

rmoriebricklayer 0.3.0

Capsule-level integrity

Portal coverage

rmoriebricklayer 0.2.5

rmoriebricklayer 0.2.4

rmoriebricklayer 0.2.3

mirror server hosted at Truenetwork, Russian Federation.